Repository: disintar/toncli (language: python)

Issue:
Add working examples of NFT and Jettons for toncli
Most work done by @Trinketer22 

But we want to add examples of NFT and Jettons with tests to toncli, so anyone can deploy those contracts via toncli
Improvement justifying abstract deployer usage
This gotta make a little bit more sense.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index f925631..2690cad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -144,3 +144,31 @@ cython_debug/
 
 .idea/
 /src/toncli/projects/external_code/build/contract.pk
+
+src/toncli/projects/external_code/build/*
+!src/toncli/projects/external_code/build/.gitkeep
+!src/toncli/projects/external_code/build/boc/.gitkeep
+
+src/toncli/projects/external_data/build/*
+!src/toncli/projects/external_data/build/.gitkeep
+!src/toncli/projects/external_data/build/boc/.gitkeep
+
+src/toncli/projects/nft_collection/build/*
+!src/toncli/projects/nft_collection/build/.gitkeep
+!src/toncli/projects/nft_collection/build/boc/.gitkeep
+
+src/toncli/projects/nft_item/build/*
+!src/toncli/projects/nft_item/build/.gitkeep
+!src/toncli/projects/nft_item/build/boc/.gitkeep
+
+src/toncli/projects/jetton_minter/build/*
+!src/toncli/projects/jetton_minter/build/.gitkeep
+!src/toncli/projects/jetton_minter/build/boc/.gitkeep
+
+src/toncli/projects/jetton_wallet/build/*
+!src/toncli/projects/jetton_wallet/build/.gitkeep
+!src/toncli/projects/jetton_wallet/build/boc/.gitkeep
+
+src/toncli/projects/wallet/build/*
+!src/toncli/projects/wallet/build/.gitkeep
+!src/toncli/projects/wallet/build/boc/.gitkeep
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 449cd34..9c46a00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,31 @@
 ## Change Log
 
+### 0.0.4
+
+Most work was supported by Ton Foundation and done by contributors, thanks.
+
+- [x] Massive refactoring, special thanks to [miroslav-tashonov](https://github.com/miroslav-tashonov)
+- [x] New tests engine (this is very awesome), special thanks to [BorysMinaiev](https://github.com/BorysMinaiev)
+  , [SpyCheese](https://github.com/SpyCheese), [EmelyanenkoK](https://github.com/EmelyanenkoK)
+- [x] Tests helpers with a lot of examples of parsing most of usable cells (could be found in `lib/tests-libs/`),
+  special thanks to [miroslav-tashonov](https://github.com/miroslav-tashonov)
+  and [Trinketer22](https://github.com/Trinketer22/)
+- [x] NFT, Jettons project example with a lot of tests on new engine, special thanks
+  to [Trinketer22](https://github.com/Trinketer22)
+
 ### 0.0.32
 
 - [x] Add `sendboc` command, so if `.boc` file pass - will send it via lite-cliente, if `.fif` file pass - will
   invoke `tncli fift sendboc` so fift script will be run, and then boc file on output will be sended
-- [x] Add `--data-params` to build command, to auto-generate data cells 
+- [x] Add `--data-params` to build command, to auto-generate data cells
 - [x] Update `stdlib.fc` to support nft
-- [x] Critical fix: `self.wait_for_deploy(contracts=real_contracts)` instead of `self.wait_for_deploy(contracts=contracts)`
+- [x] Critical fix: `self.wait_for_deploy(contracts=real_contracts)` instead
+  of `self.wait_for_deploy(contracts=contracts)`
 - [x] Add `.gitignore` to protect contract keys leaks
 - [x] Add cell slice parser in lite-client
 - [x] Add parse address to `TonUtils.fif`
 - [x] Fix custom path to `fift` / `func` / `lite-client` on startup
-- [x] Add command wallet to check wallets addresses 
+- [x] Add command wallet to check wallets addresses
 - [x] Force `fift` / `func` libs update
 - [x] More stability in lite-client interaction
 - [x] Send `exteranal_message` to contract without need to provide wrapper of `external_message`, just pass body bytes (
diff --git a/INSTALLATION.md b/INSTALLATION.md
index a4ddaf5..8179569 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -1,40 +1,52 @@
+## Installation from pre-build
 
+### Docker: linux / macOS (m1 supported)
 
+Special thanks to [Trinketer22](https://github.com/Trinketer22/) contributor we have Docker image with docker-hub:
 
-## Installation from pre-build
+1. Docker-hub pre-build images can be founded [here](https://hub.docker.com/r/trinketer22/func_docker/)
+2. Docker files with instructions can be founded [here](https://github.com/Trinketer22/func_docker)
 
 ### Linux / macOS (intel)
 
-1. Download needed prebuild for Linux : [here](https://github.com/SpyCheese/ton/actions/runs/2585669126), for Mac : [here](https://github.com/SpyCheese/ton/actions/runs/2618664609)
-   1. You need to be logged-in to GitHub to download pre-builds
+1. Download needed special pre-builds (from SpyCheese repo) for
+   Linux : [here](https://github.com/SpyCheese/ton/actions/runs/2585669126), for
+   Mac : [here](https://github.com/SpyCheese/ton/actions/runs/2618664609)
+    1. You need to be logged-in to GitHub to download pre-builds
 2. Install `Python3.9` or higher
 3. Run `pip install toncli` or `pip3 install toncli`
-   1. If you see `WARNING: The script toncli is installed in '/bla/bla/bla/Python/3.9/bin' which is not on PATH.` please manually add absolute `bin` path to PATH env
+    1. If you see `WARNING: The script toncli is installed in '/bla/bla/bla/Python/3.9/bin' which is not on PATH.`
+       please manually add absolute `bin` path to PATH env
 4. Run `toncli` and pass absolute path to `func` / `fift` / `lite-client` from first step
 
 ### Windows
 
-1. You need to download the latest version of python (3.9+) from the official website, you can do this [here](https://www.python.org/downloads/)
-   1. Please, **don't** install Microsoft Store version. It will not work.
-2. During installation, on the first screen, you need to click the "Add Python to PATH" checkbox, this is very important!
+1. You need to download the latest version of python (3.9+) from the official website, you can do
+   this [here](https://www.python.org/downloads/)
+    1. Please, **don't** install Microsoft Store version. It will not work.
+2. During installation, on the first screen, you need to click the "Add Python to PATH" checkbox, this is very
+   important!
 
 ![image](https://user-images.githubusercontent.com/19264196/160259049-8ed99862-a765-4653-84cb-b6818c0aa0b3.png)
 
-3. After successful installation, open the console (you can do this by pressing `Win+X`, selecting `Windows Terminal` in the menu)
+3. After successful installation, open the console (you can do this by pressing `Win+X`, selecting `Windows Terminal` in
+   the menu)
 
 4. Install `toncli` by running `pip install toncli`
 
-5. Download the compiled TON binaries from [here](https://github.com/SpyCheese/ton/actions/runs/2618774052) (you need to be logged in to GitHub)
+5. Download the compiled TON binaries from [here](https://github.com/SpyCheese/ton/actions/runs/2618774052) (you need to
+   be logged in to GitHub)
 
 ![image](https://user-images.githubusercontent.com/19264196/160259203-07fd0e26-9b8e-4aff-b4f0-8e4e6f871088.png)
 
-6. Unzip the downloaded archive 
+6. Unzip the downloaded archive
 
 7. Add [libcrypto-1_1-x64.dll](https://disk.yandex.ru/d/BJk7WPwr_JT0fw) to unziped files
 
 ![image](https://user-images.githubusercontent.com/19264196/160259288-3af468d7-74ac-45cb-9001-9f2604cf4119.png)
 
-9. Open the folder in the console (right mouse button, open in the terminal in Windows 11, Windows 10 and less - copy the path in Explorer and in the PowerShell (win+x) and run `cd <<copied path>>`)
+9. Open the folder in the console (right mouse button, open in the terminal in Windows 11, Windows 10 and less - copy
+   the path in Explorer and in the PowerShell (win+x) and run `cd <<copied path>>`)
 
 10. Run `toncli` in console, it will automatically detect `.exe` files and added paths to config:
 
@@ -42,15 +54,17 @@
 
 11. Success!
 
-
 ## Installation from source
 
 ### Linux & macOS
 
 1. Follow official [docs](https://ton.org/docs/#/compile) to compile sources
-   1. If you use arch linux you may use [AUR package](https://aur.archlinux.org/packages/ton-git)
+    1. If you use arch linux you may use [AUR package](https://aur.archlinux.org/packages/ton-git) (please keep in mind
+       that you need compile SpyCheese repo toncli-local branch
+       => [https://github.com/SpyCheese/ton/tree/toncli-local](https://github.com/SpyCheese/ton/tree/toncli-local))
 2. Run `pip install toncli` or `pip3 install toncli`
-   1. If you see `WARNING: The script toncli is installed in '/bla/bla/bla/Python/3.9/bin' which is not on PATH.` please manually add absolute `bin` path to PATH env
+    1. If you see `WARNING: The script toncli is installed in '/bla/bla/bla/Python/3.9/bin' which is not on PATH.`
+       please manually add absolute `bin` path to PATH env
 3. Run `toncli` and pass absolute path to `func` / `fift` / `lite-client` from first step
 
 
diff --git a/README.md b/README.md
index 19a484e..5771568 100644
--- a/README.md
+++ b/README.md
@@ -21,9 +21,7 @@ Easy to deploy and interact with TON smart contracts.
 
 ## 🔧 Installation
 
-> ⚠ If you want to support new tests engine - you need to use special binaries from SpyCheese repo. More information could be found in docs.
-
-Toncli support Windows / macOS (m1 / intel) / Linux, installation guide can be
+Toncli support Windows / macOS (m1 / intel) / Linux / Docker, installation guide can be
 found: [INSTALLATION.md](/INSTALLATION.md)
 
 ### 📚 Usage and docs
@@ -50,26 +48,22 @@ We are trying to process all tasks through issues. You can take own one 🥳
 ## Features and status
 
 | Feature                                                                                                   | Status |
-|-----------------------------------------------------------------------------------------------------------|--------|
-| `fift` / `func` / `lite-server` usage                                                                     | ✅      |
-| Easy bootstrap project samples `wallet`                                                                   | ✅      |
-| Deploy-wallet for auto send TON to contracts and tests                                                    | ✅      |
-| Compile func to `build/` from `func/` with `files.yaml`                                                   | ✅      |
-| Auto send TON to init contract address                                                                    | ✅      |
-| Deploy to mainnet / testnet / ownnet                                                                      | ✅      |
-| Project interact after deploy: easily send messages, run getmethods, run fift parsers on getmethods output | ✅      |
-| Load from hard project structure (example: `src/projects/wallet`)                                         | ✅      |
-| Run remote contracts locally (get cells from chain and run locally to get error / debug / etc.)           | ✅      |
-| Get contract address by `toncli addrs`                                                                    | ✅      |
-| Docs for contract creation for beginners                                                                  | ✅      |
-| Project tests with `runvmcode`                                                                            | ✅      |
-| Windows & Linux support                                                                                   | ✅      |
-| Unit tests engine                                                                                         | ✅      |
-| Gas auto calculation for store & deploy                                                                   | ❌      |
-| Add more project samples with advanced usage                                                              | ❌      |
-| Project debug                                                                                             | ❌      |
-| Library support                                                                                           | ❌      |
-| Init Message support  (with signature)                                                                    | ❌      |
+|-----------------------------------------------------------------------------------------------------------|-------|
+| `fift` / `func` / `lite-server` usage                                                                     | ✅     |
+| Easy bootstrap project samples `wallet`                                                                   | ✅     |
+| Deploy-wallet for auto send TON to contracts and tests                                                    | ✅     |
+| Compile func to `build/` from `func/` with `files.yaml`                                                   | ✅     |
+| Auto send TON to init contract address                                                                    | ✅     |
+| Deploy to mainnet / testnet / ownnet                                                                      | ✅     |
+| Project interact after deploy: easily send messages, run getmethods, run fift parsers on getmethods output | ✅     |
+| Load from hard project structure (example: `src/projects/wallet`)                                         | ✅     |
+| Run remote contracts locally (get cells from chain and run locally to get error / debug / etc.)           | ✅     |
+| Get contract address by `toncli addrs`                                                                    | ✅     |
+| Docs for contract creation for beginners                                                                  | ✅     |
+| Project tests with `runvmcode`                                                                            | ✅     |
+| Windows & Linux support                                                                                   | ✅     |
+| Unit tests engine                                                                                         | ✅     |
+| Add more project samples with advanced usage                                                              | ✅      |
 
 ## Commands
 
diff --git a/src/toncli/lib/fift-libs/AsmTests.fif b/src/toncli/lib/fift-libs/AsmTests.fif
index 1ade853..4e0ce26 100644
--- a/src/toncli/lib/fift-libs/AsmTests.fif
+++ b/src/toncli/lib/fift-libs/AsmTests.fif
@@ -1315,3 +1315,5 @@ variable asm-mode  1 asm-mode !
 { <b 2 8 u, swap 256 u, b>spec } : hash>libref
 // ( c -- c' )
 { hash hash>libref } : >libref
+
+true constant asm-test
\ No newline at end of file
diff --git a/src/toncli/modules/abstract/deployer.py b/src/toncli/modules/abstract/deployer.py
index 24defd7..e0f6d0b 100644
--- a/src/toncli/modules/abstract/deployer.py
+++ b/src/toncli/modules/abstract/deployer.py
@@ -134,6 +134,15 @@ def get_address(self, contracts: List[TonProjectConfig] = None) -> List[List[str
 
         return addresses
 
+    def export_address_to_fift( self, path: str, name: str = "owner" ):
+        addresses = self.get_address()
+
+        with open( path, "w", encoding='utf-8' ) as f:
+            logger.info(f"🦘 Exporing address to {gr}{path}{rs}")
+            f.write( addresses[0][0].replace( ":", " ") + f" 2constant {name}_raw\n" )
+            f.write( f'"{addresses[0][1]}" constant {name}_address\n' )
+            f.write( f'"{addresses[0][2]}" constant {name}_no_bounce\n' )
+
     def compile_func(self, contracts: List[TonProjectConfig] = None):
         """Compile func to code.fif"""
         # Build code
diff --git a/src/toncli/modules/deploy_wallet_contract.py b/src/toncli/modules/deploy_wallet_contract.py
index 37787e0..b262c03 100644
--- a/src/toncli/modules/deploy_wallet_contract.py
+++ b/src/toncli/modules/deploy_wallet_contract.py
@@ -26,6 +26,7 @@ def __init__(self, network: str, workchain: int):
         self.network = network
         self.workchain = workchain
         self.project_root = os.path.abspath(f"{config_folder}/wallet")
+        self.owner_fift_path = os.path.abspath(f"{config_folder}/fift-libs/OwnerAddr.fif")
 
         # If files.yaml in func folder - it's older version of project structure, so migrate
         if os.path.exists(os.path.abspath(f"{self.project_root}/func/files.yaml")):
@@ -61,6 +62,8 @@ def __init__(self, network: str, workchain: int):
                     f"💎 About {gr}2 TON{rs} will be OK for 10-12 contracts\n"
                     f"🧪 Test coins can be found in {bl}@testgiver_ton_bot{rs} / @tondev")
 
+
+                self.export_address_to_fift( self.owner_fift_path, "owner" );
                 sys.exit()
         else:
             self.project_config = ProjectConf(self.project_root)
@@ -72,6 +75,11 @@ def __init__(self, network: str, workchain: int):
                 f"🦘 Found existing deploy-wallet [{gr}{self.addresses[0][1]}{rs}] (Balance: {balance}💎, "
                 f"Is inited: {is_inited}) in {config_folder}")
 
+            if not os.path.exists( self.owner_fift_path ):
+                self.export_address_to_fift( self.owner_fift_path, "owner" )
+
+
+
     def send_ton(self, address: str, amount: float, quiet: bool = False):
         """Send ton to some address from DeployWallet"""
         balance, is_inited = self.get_status()[0]
diff --git a/src/toncli/modules/projects.py b/src/toncli/modules/projects.py
index 820b26b..42bd37e 100644
--- a/src/toncli/modules/projects.py
+++ b/src/toncli/modules/projects.py
@@ -22,6 +22,10 @@ def __init__(self, project_name: str, folder_name: str, location: Optional[str]
         self.project_help_action = defaultdict(lambda: "")
 
         self.project_help_action['wallet'] = f" and {gr}toncli deploy -n testnet{rs}"
+        self.project_help_action['nft_collection']= f" and {gr} cat README.md{rs} for more information"
+        self.project_help_action['nft_item']      = f" and {gr} build, run_tests (mainly for tesing/dev purposes)"
+        self.project_help_action['jetton_minter'] = f" and {gr} cat README.md{rs} for more information"
+        self.project_help_action['jetton_wallet'] = f" and {gr} build, run_tests (mainly for tesing/dev purposes)"
         self.project_help_action['external_code'] = f" and {gr}cat README.md{rs} for more information"
         self.project_help_action['external_data'] = f" and {gr}cat README.md{rs} for more information"
 
diff --git a/src/toncli/modules/utils/commands/__init__.py b/src/toncli/modules/utils/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/modules/utils/commands/command_classes/__init__.py b/src/toncli/modules/utils/commands/command_classes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/modules/utils/parsers/__init__.py b/src/toncli/modules/utils/parsers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/modules/utils/parsers/parser_utils.py b/src/toncli/modules/utils/parsers/parser_utils.py
index b22221c..d1c02c6 100644
--- a/src/toncli/modules/utils/parsers/parser_utils.py
+++ b/src/toncli/modules/utils/parsers/parser_utils.py
@@ -4,6 +4,7 @@
 from toncli.modules.utils.text.text_utils import TextUtils
 from toncli.modules.utils.system.conf import get_projects
 
+
 class ParserUtil():
     subparser: Any
 
@@ -27,22 +28,23 @@ def set_all_parsers(self):
     # define the function blocks
     def set_project_parser(self):
         parser_project = self.subparser.add_parser('start',
-                                          description='Create new project structure based on example project')
+                                                   description='Create new project structure based on example project')
         parser_project.add_argument('project', choices=get_projects(),
                                     help="Which default project to bootstrap")
 
         parser_project.add_argument("--name", "-n", default=None, type=str, help='New project folder name')
 
     def set_deploy_parser(self):
-        parser_deploy = self.subparser.add_parser('deploy', description='Deploy project to blockchain')
+        parser_deploy = self.subparser.add_parser('deploy',
+                                                  description='Deploy project to blockchain, specify contract_name after command if special contract needed')
         parser_deploy.add_argument("--net", "-n", default='testnet', type=str, choices=['testnet', 'mainnet', 'ownnet'],
-                                help='Network to deploy')
+                                   help='Network to deploy')
         parser_deploy.add_argument("--workchain", "-wc", default=0, type=int, help='Workchain deploy to')
         parser_deploy.add_argument("--ton", "-t", default=0.05, type=float,
-                                help='How much TON will be sent to new contract')
+                                   help='How much TON will be sent to new contract')
         parser_deploy.add_argument("--update", action='store_true', help='Update cached configs of net')
         parser_deploy.add_argument('--data-params', help='Data which you want to pass to data of your smart-contract',
-                                default="", type=str)
+                                   default="", type=str)
 
     def set_get_parser(self):
         parser_get = self.subparser.add_parser('get', description='Run get method on contract')
@@ -61,14 +63,14 @@ def set_send_parser(self):
         parser_send.add_argument("--amount", "-a", type=float, default=0, help='How much TON need to send')
         parser_send.add_argument("--contracts", "-c", type=str, help='Set contract name from project.yaml to send to')
         parser_send.add_argument("--net", "-n", default='testnet', type=str, choices=['testnet', 'mainnet', 'ownnet'],
-                                help='Network to use')
+                                 help='Network to use')
         parser_send.add_argument("--update", action='store_true', help='Update cached configs of net')
         parser_send.add_argument("--address", type=str,
-                                help='Set contract address to run get method on')
+                                 help='Set contract address to run get method on')
 
         parser_send.add_argument("--mode", type=int, help='Sets transfer mode (0..255) for SENDRAWMSG')
         parser_send.add_argument("--body", "-b", type=str,
-                                help='Path to fift file to get body from (need to set cell in the end of stack)')
+                                 help='Path to fift file to get body from (need to set cell in the end of stack)')
         parser_send.add_argument("--no-bounce", "-nb", type=int, help='Clears bounce flag')
         parser_send.add_argument("--force-bounce", "-fb", type=int, help='Forces bounce flag')
 
@@ -77,9 +79,9 @@ def set_tointeger_parser(self):
 
     def set_runtransation_parser(self):
         parser_run_transaction = self.subparser.add_parser('run_transaction',
-                                                  description='Message debug - by lt / transaction hash / '
-                                                              'smart contract address'
-                                                              ' - run message locally and get stack error')
+                                                           description='Message debug - by lt / transaction hash / '
+                                                                       'smart contract address'
+                                                                       ' - run message locally and get stack error')
         parser_run_transaction.add_argument("logical_time", type=str)
         parser_run_transaction.add_argument("transaction_hash", type=str)
         parser_run_transaction.add_argument("smc_address", type=str)
@@ -90,49 +92,49 @@ def set_runtransation_parser(self):
                                             help='Function selector on runvm (-1 - external message, 0 - internal, ...')
         parser_run_transaction.add_argument("--save", "-s", default=None, type=str,
                                             help='Pass save location, so runvm script will not run and just save to '
-                                                'your location')
+                                                 'your location')
 
     def set_shorcuts_parser(self):
         self.subparser.add_parser('f', help="Same as fift",
-                         formatter_class=argparse.RawDescriptionHelpFormatter,
-                         description=textwrap.dedent(TextUtils.FIFT_HELP))
+                                  formatter_class=argparse.RawDescriptionHelpFormatter,
+                                  description=textwrap.dedent(TextUtils.FIFT_HELP))
         self.subparser.add_parser('fc', help="Same as func",
-                            formatter_class=argparse.RawDescriptionHelpFormatter,
-                            description=textwrap.dedent(TextUtils.FUNC_HELP))
+                                  formatter_class=argparse.RawDescriptionHelpFormatter,
+                                  description=textwrap.dedent(TextUtils.FUNC_HELP))
         self.subparser.add_parser('lc', help="Same as lite-client",
-                            formatter_class=argparse.RawDescriptionHelpFormatter,
-                            description=textwrap.dedent(TextUtils.LITE_CLIENT_HELP))
+                                  formatter_class=argparse.RawDescriptionHelpFormatter,
+                                  description=textwrap.dedent(TextUtils.LITE_CLIENT_HELP))
         self.subparser.add_parser('run', help="Same as fift run",
-                            formatter_class=argparse.RawDescriptionHelpFormatter,
-                            description=textwrap.dedent(TextUtils.FIFT_HELP))
+                                  formatter_class=argparse.RawDescriptionHelpFormatter,
+                                  description=textwrap.dedent(TextUtils.FIFT_HELP))
         self.subparser.add_parser('build', help="Same as func build",
-                            formatter_class=argparse.RawDescriptionHelpFormatter,
-                            description=textwrap.dedent(TextUtils.FIFT_HELP))
+                                  formatter_class=argparse.RawDescriptionHelpFormatter,
+                                  description=textwrap.dedent(TextUtils.FIFT_HELP))
 
     def set_fift_parser(self):
         parser_fift = self.subparser.add_parser('fift', help=TextUtils.FIFT_HELP,
-                                       formatter_class=argparse.RawDescriptionHelpFormatter,
-                                       description=textwrap.dedent(TextUtils.FIFT_HELP))
+                                                formatter_class=argparse.RawDescriptionHelpFormatter,
+                                                description=textwrap.dedent(TextUtils.FIFT_HELP))
         parser_fift.add_argument("--net", "-n", default='testnet', type=str, choices=['testnet', 'mainnet', 'ownnet'],
-                                help='Network to deploy')
+                                 help='Network to deploy')
         parser_fift.add_argument("--workchain", "-wc", default=0, type=int, help='Workchain deploy to')
         parser_fift.add_argument("--update", action='store_true', default=False, help='Update cached configs of net')
         parser_fift.add_argument("--build", action='store_true', default=False,
-                                help='Build func code from func/ folder in project')
+                                 help='Build func code from func/ folder in project')
         parser_fift.add_argument("--fift-args", "-fa", type=str, default='',
-                                help='Pass args and kwargs to fift command, e.g.: -fa "-v 4" - '
-                                    'set verbose level, will overwrite default ones, '
-                                    'if you want pass args after command just don\'t use flag, f.e.g. '
-                                    '[toncli fift run wallet.fif 0 0 1 -v 4]')
+                                 help='Pass args and kwargs to fift command, e.g.: -fa "-v 4" - '
+                                      'set verbose level, will overwrite default ones, '
+                                      'if you want pass args after command just don\'t use flag, f.e.g. '
+                                      '[toncli fift run wallet.fif 0 0 1 -v 4]')
         parser_fift.add_argument("--lite-client-args", "-la", type=str,
-                                default='',
-                                help='Pass args and kwargs to lite-client command in sendboc mode, '
-                                    'e.g.: -la "-v 4" - set verbose level')
+                                 default='',
+                                 help='Pass args and kwargs to lite-client command in sendboc mode, '
+                                      'e.g.: -la "-v 4" - set verbose level')
 
     def set_liteclient_parser(self):
         parser_lite_client = self.subparser.add_parser('lite-client', help=TextUtils.LITE_CLIENT_HELP,
-                                              formatter_class=argparse.RawDescriptionHelpFormatter,
-                                              description=textwrap.dedent(TextUtils.LITE_CLIENT_HELP))
+                                                       formatter_class=argparse.RawDescriptionHelpFormatter,
+                                                       description=textwrap.dedent(TextUtils.LITE_CLIENT_HELP))
         parser_lite_client.add_argument("--net", "-n", default='testnet', type=str,
                                         choices=['testnet', 'mainnet', 'ownnet'],
                                         help='Network to deploy')
@@ -171,15 +173,15 @@ def set_updatelibs_parser(self):
 
     def set_func_parser(self):
         parser_func = self.subparser.add_parser('func', help=TextUtils.FUNC_HELP,
-                                       formatter_class=argparse.RawDescriptionHelpFormatter,
-                                       description=textwrap.dedent(TextUtils.FUNC_HELP))
+                                                formatter_class=argparse.RawDescriptionHelpFormatter,
+                                                description=textwrap.dedent(TextUtils.FUNC_HELP))
         parser_func.add_argument("--func-args", "-fca", type=str,
-                                default='',
-                                help='Pass arguments to func command')
+                                 default='',
+                                 help='Pass arguments to func command')
         parser_func.add_argument("--fift-args", "-fa", type=str, default='',
-                                help='Pass args and kwargs to fift command, e.g.: -fa "-v 4" - '
-                                    'set verbose level, will overwrite default ones, '
-                                    'if you want pass args after command just don\'t use flag, f.e.g. '
-                                    '[toncli fift run wallet.fif 0 0 1 -v 4]')
+                                 help='Pass args and kwargs to fift command, e.g.: -fa "-v 4" - '
+                                      'set verbose level, will overwrite default ones, '
+                                      'if you want pass args after command just don\'t use flag, f.e.g. '
+                                      '[toncli fift run wallet.fif 0 0 1 -v 4]')
         parser_func.add_argument("--run", "-r", action='store_true', default=False,
-                                help='Run fift code that was generated in build mode')
+                                 help='Run fift code that was generated in build mode')
diff --git a/src/toncli/modules/utils/text/__init__.py b/src/toncli/modules/utils/text/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/modules/utils/text/text_utils.py b/src/toncli/modules/utils/text/text_utils.py
index ee1ff74..3e863b1 100644
--- a/src/toncli/modules/utils/text/text_utils.py
+++ b/src/toncli/modules/utils/text/text_utils.py
@@ -34,6 +34,10 @@ class TextUtils:
 
         {bl}start - create new project structure based on example project  
         {gr}   wallet - create project with v3 wallet example
+        {gr}   nft_collection- project with NFT collection example
+        {gr}   nft_item      - project with single nft item example
+        {gr}   jetton_minter - project with Jetton minter example
+        {gr}   jetton_wallet - project with Jetton wallet example
         {gr}   external_data - create external data usage example
         {gr}   external_code - create external code usage example
 
diff --git a/src/toncli/modules/utils/ton/__init__.py b/src/toncli/modules/utils/ton/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/.gitignore b/src/toncli/projects/jetton_minter/.gitignore
new file mode 100644
index 0000000..378b0b4
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/.gitignore
@@ -0,0 +1,1 @@
+*.pk
diff --git a/src/toncli/projects/jetton_minter/README.md b/src/toncli/projects/jetton_minter/README.md
new file mode 100644
index 0000000..b87aa56
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/README.md
@@ -0,0 +1,59 @@
+# Jetton Minter example project
+
+This project allows you to:
+
+1.  Build basic jetton minter contract
+2.  Aims to *hopefully* test any nftcollection contract for compliance with [Jetton standerd](https://github.com/ton-blockchain/TIPs/issues/74)
+3.  Deploy minter contract via `toncli deploy`
+4.  Manually deploy jetton wallet via minting tokens
+5.  Manually send to other jetton wallets
+6.  Manyally burn coins on your wallet
+
+## Building
+
+  `toncli start jetton_minter`  
+  `toncli build`  
+
+## Testing
+
+  Same here `toncli run_test`  
+  If you encounter **error 6** during *run_tests*
+  make shure that your binaries are built according to:[this manual](https://github.com/disintar/toncli/blob/master/docs/advanced/func_tests_new.md)
+  
+## Deploying minter contract
+
+  This project consists of two sub-projects **jetton_minter** and **jetton_wallet**
+  You can see that in the *project.yml*
+  **BOTH** of those have to be built.
+  First type:`toncli build`
+  However it makes sense to deploy only *jetton_minter*.  
+  Prior to deployment you need to check out *fift/minter_data.fif*
+  and change all mock configuration values to your own liking.  
+  To deploy run:`toncli deploy -n testnet jetton_minter`.  
+  
+## Minting jettons
+
+  To mint coins to your wallet
+  you will have to:  
+  
++   Configure *fift/mint_jettons.fif* script with your own values:
+[Take a look](https://github.com/ton-blockchain/TIPs/issues/74)  
+
++   Make yourself familiar with process of sending  [internal messages](https://github.com/disintar/toncli/blob/master/docs/advanced/send_fift_internal.md)  
+
+`toncli send -n testnet -a 0.035 -c jetton_minter  --body fift/mint_jettons.fif`
+
+## Sending jettons
+
+  To send coins to someone elses jetton wallet
+  you will have to:
+
++   Setup values in *fift/send_jettons.fif*  
++   Run:`toncli send -n testnet -a 0.1 --address < your jetton wallet addr>  --body fift/send_jettons.fif`  
+
+## Burning jettons
+
+  To burn jettons
+
++   Setup values in *fift/burn_jettons.fif*
++   Run `toncli send -n testnet -a 0.1 --address < your jetton wallet addr >  --body fift/burn_jettons.fif`
diff --git a/src/toncli/projects/jetton_minter/build/.gitkeep b/src/toncli/projects/jetton_minter/build/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/build/boc/.gitkeep b/src/toncli/projects/jetton_minter/build/boc/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/fift/.gitkeep b/src/toncli/projects/jetton_minter/fift/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/fift/burn_jettons.fif b/src/toncli/projects/jetton_minter/fift/burn_jettons.fif
new file mode 100644
index 0000000..bfd6c35
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/fift/burn_jettons.fif
@@ -0,0 +1,8 @@
+"TonUtil.fif" include
+
+<b
+  0x595f07bc 32 u, // op::burn
+  12345 64 u, // query_id
+  5000000000 Gram, // Burning 5 jettons
+  0 3 u, // addr_none for resp_dst and no custom payload
+b>
diff --git a/src/toncli/projects/jetton_minter/fift/mint_jettons.fif b/src/toncli/projects/jetton_minter/fift/mint_jettons.fif
new file mode 100644
index 0000000..6bbcdf2
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/fift/mint_jettons.fif
@@ -0,0 +1,33 @@
+"TonUtil.fif" include
+
+"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant mint_address
+0x178d4519 constant internal_transfer
+1000000000 10 * constant mint_amount 
+30000000 constant forward_amount
+
+mint_address
+$>smca 0= abort"Specify valid mint addr"
+drop  // Drop flags
+
+2constant mint_raw // worchain and addr into single constant
+
+// Here goes master message
+// internal_transfer
+
+<b
+  internal_transfer 32 u, // op internal_transfer
+  4321 64 u, // query_id
+  mint_amount Gram, // 10 jettons
+  mint_raw Addr, // From addr
+  0 2 u, // addr_none for simplicity Address vwhere transfer notification would be sent
+  0 Gram, // No address no forward
+  0 1 u, // normal forward payload
+b>
+
+<b
+  21 32 u, // op mint
+  1234 64 u, // query_id
+  mint_raw Addr,
+  forward_amount Gram,
+  swap ref, // master message ref
+b>
diff --git a/src/toncli/projects/jetton_minter/fift/minter_data.fif b/src/toncli/projects/jetton_minter/fift/minter_data.fif
new file mode 100644
index 0000000..5382599
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/fift/minter_data.fif
@@ -0,0 +1,28 @@
+"TonUtil.fif" include
+"Asm.fif" include
+
+
+"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant owner_address // Specify your own
+"https://raw.githubusercontent.com/Trinketer22/token-contract/main/ft/web-example/test_jetton.json" constant jetton_meta // Specify your own
+"build/jetton_wallet.fif" constant wallet_code_path
+1000000000 100 * constant jetton_supply // Starting jetton supply
+
+<b
+  1 8 u, // Off-chain token
+  jetton_meta
+  $>B B,
+b>
+
+
+owner_address
+$>smca 0= abort"Specify valid admin addr"
+drop // Drop flags
+
+<b
+  jetton_supply Gram, // total supply
+  -rot Addr,  //  admin address
+  swap ref,   //   content cell
+  wallet_code_path include // Loading jetton code
+  over tuple? { nip } if // In testing mode (asm-mode 3) include returns ( cell, tuple ) we don't want tuple in the cell
+  ref,   //   code cell
+b>
diff --git a/src/toncli/projects/jetton_minter/fift/send_jettons.fif b/src/toncli/projects/jetton_minter/fift/send_jettons.fif
new file mode 100644
index 0000000..9574f61
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/fift/send_jettons.fif
@@ -0,0 +1,27 @@
+"TonUtil.fif" include
+
+"EQDRebAnF1pvH1YsKNp7mtpsz+CLs6WxaffUojt1ijyrazkg" constant receiver_address
+1000000000 constant send_amount
+20000000 constant forward_amount
+12345 constant query_id
+0xf8a7ea5 constant op_transfer
+
+receiver_address
+$>smca 0= abort"Specify valid send addr"
+drop  // Drop flags
+
+2constant send_addr // worchain and addr into single constant
+<b
+  op_transfer 32 u, // op transfer
+  query_id 64 u, // query_id
+  send_amount Gram, // Sending one jetton
+  send_addr Addr,
+  0 2 u, // addr_none for resp_dst
+  0 1 u, // No custom payload
+  forward_amount Gram, // forward ton amount 0.03
+  0 1 u, // no ref forward_payload
+b>
+// Here goes master message
+// internal_transfer
+
+
diff --git a/src/toncli/projects/jetton_minter/fift/wallet_data.fif b/src/toncli/projects/jetton_minter/fift/wallet_data.fif
new file mode 100644
index 0000000..64abbf5
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/fift/wallet_data.fif
@@ -0,0 +1,30 @@
+"TonUtil.fif" include
+"Asm.fif" include
+
+1000000000 10 * constant initial_balance // Wallet balance 
+"build/jetton_wallet.fif" constant wallet_code
+
+/* 
+ This file is primerely for tests to run
+ Main way of deploying jetton wallet is via jetton minter "mint" operation.
+ Sending jettons from already deployed wallet is also an option
+ Thus random 256 bit value will be used ans and address for collection and owner
+
+ We're going to use newkeypair as a way to generate two random 256 bit values
+ Do not do that in real app 
+*/
+
+newkeypair 
+
+256 B>u@
+swap
+256 B>u@
+
+<b
+  initial_balance Gram, // 10 jettons balance ( if decimals is set to 9 )
+  Basechain rot Addr, // owner
+  Basechain rot Addr, // minter addr
+  wallet_code include
+  over tuple? { nip } if // In testing mode include will produce tuple with method names we don't want that in the cell 
+  ref, // wallet code
+b>
diff --git a/src/toncli/projects/jetton_minter/func/helpers.func b/src/toncli/projects/jetton_minter/func/helpers.func
new file mode 100644
index 0000000..8cf2360
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/helpers.func
@@ -0,0 +1,2 @@
+int equal_slices (slice a, slice b) asm "SDEQ";
+int tuple_length( tuple t ) asm "TLEN";
diff --git a/src/toncli/projects/jetton_minter/func/jetton-minter.func b/src/toncli/projects/jetton_minter/func/jetton-minter.func
new file mode 100644
index 0000000..bde1b1b
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/jetton-minter.func
@@ -0,0 +1,122 @@
+;; Jettons minter smart contract
+
+;; storage scheme
+;; storage#_ total_supply:Coins admin_address:MsgAddress content:^Cell jetton_wallet_code:^Cell = Storage;
+
+#pragma version >=0.2.0;
+#include "op-codes.func";
+#include "params.func";
+#include "helpers.func";
+#include "jetton-utils.func";
+
+
+
+(int, slice, cell, cell) load_data() inline {
+  slice ds = get_data().begin_parse();
+  return (
+      ds~load_coins(), ;; total_supply
+      ds~load_msg_addr(), ;; admin_address
+      ds~load_ref(), ;; content
+      ds~load_ref()  ;; jetton_wallet_code
+  );
+}
+
+() save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline {
+  set_data(begin_cell()
+            .store_coins(total_supply)
+            .store_slice(admin_address)
+            .store_ref(content)
+            .store_ref(jetton_wallet_code)
+           .end_cell()
+          );
+}
+
+() mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure {
+  cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code);
+  slice to_wallet_address = calculate_jetton_wallet_address(state_init);
+  var msg = begin_cell()
+    .store_uint(0x18, 6)
+    .store_slice(to_wallet_address)
+    .store_coins(amount)
+    .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
+    .store_ref(state_init)
+    .store_ref(master_msg);
+  send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors
+}
+
+() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+    slice cs = in_msg_full.begin_parse();
+    int flags = cs~load_uint(4);
+
+    if (flags & 1) { ;; ignore all bounced messages
+        return ();
+    }
+    slice sender_address = cs~load_msg_addr();
+  
+    int op = in_msg_body~load_uint(32);
+    int query_id = in_msg_body~load_uint(64);
+
+    (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
+
+    if (op == op_mint) {
+        throw_unless(73, equal_slices(sender_address, admin_address));
+        slice to_address = in_msg_body~load_msg_addr();
+        int amount = in_msg_body~load_coins();
+        cell master_msg = in_msg_body~load_ref();
+        slice master_msg_cs = master_msg.begin_parse();
+        master_msg_cs~skip_bits(32 + 64); ;; op + query_id
+        int jetton_amount = master_msg_cs~load_coins();
+        mint_tokens(to_address, jetton_wallet_code, amount, master_msg);
+        save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code);
+        return ();
+    }
+
+    if (op == op_burn_notification) {
+        int jetton_amount = in_msg_body~load_coins();
+        slice from_address = in_msg_body~load_msg_addr();
+        throw_unless(74,
+            equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address)
+        );
+        save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code);
+        slice response_address = in_msg_body~load_msg_addr();
+        if (response_address.preload_uint(2) != 0) {
+          var msg = begin_cell()
+            .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
+            .store_slice(response_address)
+            .store_coins(0)
+            .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+            .store_uint(op_excesses, 32)
+            .store_uint(query_id, 64);
+          send_raw_message(msg.end_cell(), 2 + 64);
+        }
+        return ();
+    }
+
+    if (op == 3) { ;; change admin
+        throw_unless(73, equal_slices(sender_address, admin_address));
+        slice new_admin_address = in_msg_body~load_msg_addr();
+        save_data(total_supply, new_admin_address, content, jetton_wallet_code);
+        return ();
+    }
+
+    if (op == 4) { ;; change content, delete this for immutable tokens
+        throw_unless(73, equal_slices(sender_address, admin_address));
+        save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code);
+        return ();
+    }
+
+    throw(0xffff);
+}
+
+(int, int, slice, cell, cell) get_jetton_data() method_id {
+    (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
+    return (total_supply, -1, admin_address, content, jetton_wallet_code);
+}
+
+slice get_wallet_address(slice owner_address) method_id {
+    (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data();
+    return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code);
+}
diff --git a/src/toncli/projects/jetton_minter/func/jetton-utils.func b/src/toncli/projects/jetton_minter/func/jetton-utils.func
new file mode 100644
index 0000000..fda1c8d
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/jetton-utils.func
@@ -0,0 +1,34 @@
+#pragma version >=0.2.0;
+#include "params.func";
+
+
+cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+   return  begin_cell()
+            .store_coins(balance)
+            .store_slice(owner_address)
+            .store_slice(jetton_master_address)
+            .store_ref(jetton_wallet_code)
+           .end_cell();
+}
+
+cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+  return begin_cell()
+          .store_uint(0, 2)
+          .store_dict(jetton_wallet_code)
+          .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code))
+          .store_uint(0, 1)
+         .end_cell();
+}
+
+slice calculate_jetton_wallet_address(cell state_init) inline {
+  return begin_cell().store_uint(4, 3)
+                     .store_int(workchain, 8)
+                     .store_uint(cell_hash(state_init), 256)
+                     .end_cell()
+                     .begin_parse();
+}
+
+slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+  return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code));
+}
+
diff --git a/src/toncli/projects/jetton_minter/func/jetton-wallet.func b/src/toncli/projects/jetton_minter/func/jetton-wallet.func
new file mode 100644
index 0000000..3e82459
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/jetton-wallet.func
@@ -0,0 +1,251 @@
+;; Jetton Wallet Smart Contract
+
+{-
+
+NOTE that this tokens can be transferred within the same workchain.
+
+This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions:
+
+1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`)
+
+2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain)
+
+-}
+
+#pragma version >=0.2.0;
+#include "op-codes.func";
+#include "params.func";
+#include "helpers.func";
+#include "jetton-utils.func";
+
+
+
+
+const int min_tons_for_storage = 10000000; ;; 0.01 TON
+const int gas_consumption = 10000000; ;; 0.01 TON
+
+{-
+  Storage
+  storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage;
+-}
+
+(int, slice, slice, cell) load_data() inline {
+  slice ds = get_data().begin_parse();
+  return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
+}
+
+() save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline {
+  set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code));
+}
+
+{-
+  transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
+           response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+           forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+           = InternalMsgBody;
+  internal_transfer  query_id:uint64 amount:(VarUInteger 16) from:MsgAddress
+                     response_address:MsgAddress
+                     forward_ton_amount:(VarUInteger 16)
+                     forward_payload:(Either Cell ^Cell) 
+                     = InternalMsgBody;
+-}
+
+() send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  slice to_owner_address = in_msg_body~load_msg_addr();
+  force_chain(to_owner_address);
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  balance -= jetton_amount;
+
+  throw_unless(705, equal_slices(owner_address, sender_address));
+  throw_unless(706, balance >= 0);
+
+  cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
+  slice to_wallet_address = calculate_jetton_wallet_address(state_init);
+  slice response_address = in_msg_body~load_msg_addr();
+  cell custom_payload = in_msg_body~load_dict();
+  int forward_ton_amount = in_msg_body~load_coins();
+  throw_unless(708, slice_bits(in_msg_body) >= 1);
+  slice either_forward_payload = in_msg_body;
+  var msg = begin_cell()
+    .store_uint(0x18, 6)
+    .store_slice(to_wallet_address)
+    .store_coins(0)
+    .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
+    .store_ref(state_init);
+  var msg_body = begin_cell()
+    .store_uint(op_internal_transfer, 32)
+    .store_uint(query_id, 64)
+    .store_coins(jetton_amount)
+    .store_slice(owner_address)
+    .store_slice(response_address)
+    .store_coins(forward_ton_amount)
+    .store_slice(either_forward_payload)
+    .end_cell();
+
+  msg = msg.store_ref(msg_body);
+  int fwd_count = forward_ton_amount ? 2 : 1;
+  throw_unless(709, msg_value >
+                     forward_ton_amount +
+                     ;; 3 messages: wal1->wal2,  wal2->owner, wal2->response
+                     ;; but last one is optional (it is ok if it fails)
+                     fwd_count * fwd_fee +
+                     (2 * gas_consumption + min_tons_for_storage));
+                     ;; universal message send fee calculation may be activated here
+                     ;; by using this instead of fwd_fee
+                     ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15)
+
+  send_raw_message(msg.end_cell(), 64); ;; revert on errors
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+{-
+  internal_transfer  query_id:uint64 amount:(VarUInteger 16) from:MsgAddress
+                     response_address:MsgAddress
+                     forward_ton_amount:(VarUInteger 16)
+                     forward_payload:(Either Cell ^Cell) 
+                     = InternalMsgBody;
+-}
+
+() receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure {
+  ;; NOTE we can not allow fails in action phase since in that case there will be
+  ;; no bounce. Thus check and throw in computation phase.
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  balance += jetton_amount;
+  slice from_address = in_msg_body~load_msg_addr();
+  slice response_address = in_msg_body~load_msg_addr();
+  throw_unless(707,
+      equal_slices(jetton_master_address, sender_address)
+      |
+      equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address)
+  );
+  int forward_ton_amount = in_msg_body~load_coins();
+
+  int ton_balance_before_msg = my_ton_balance - msg_value;
+  int storage_fee = min_tons_for_storage - min(ton_balance_before_msg, min_tons_for_storage);
+  msg_value -= (storage_fee + gas_consumption);
+  if(forward_ton_amount) {
+    msg_value -= (forward_ton_amount + fwd_fee);
+    slice either_forward_payload = in_msg_body;
+
+    var msg_body = begin_cell()
+        .store_uint(op_transfer_notification, 32)
+        .store_uint(query_id, 64)
+        .store_coins(jetton_amount)
+        .store_slice(from_address)
+        .store_slice(either_forward_payload)
+        .end_cell();
+
+    var msg = begin_cell()
+      .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
+      .store_slice(owner_address)
+      .store_coins(forward_ton_amount)
+      .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+      .store_ref(msg_body);
+
+    send_raw_message(msg.end_cell(), 1);
+  }
+
+  if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
+    var msg = begin_cell()
+      .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
+      .store_slice(response_address)
+      .store_coins(msg_value)
+      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+      .store_uint(op_excesses, 32)
+      .store_uint(query_id, 64);
+    send_raw_message(msg.end_cell(), 2);
+  }
+
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
+  ;; NOTE we can not allow fails in action phase since in that case there will be
+  ;; no bounce. Thus check and throw in computation phase.
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  slice response_address = in_msg_body~load_msg_addr();
+  ;; ignore custom payload
+  ;; slice custom_payload = in_msg_body~load_dict();
+  balance -= jetton_amount;
+  throw_unless(705, equal_slices(owner_address, sender_address));
+  throw_unless(706, balance >= 0);
+  throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption);
+
+  var msg_body = begin_cell()
+      .store_uint(op_burn_notification, 32)
+      .store_uint(query_id, 64)
+      .store_coins(jetton_amount)
+      .store_slice(owner_address)
+      .store_slice(response_address)
+      .end_cell();
+
+  var msg = begin_cell()
+    .store_uint(0x18, 6)
+    .store_slice(jetton_master_address)
+    .store_coins(0)
+    .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+    .store_ref(msg_body);
+
+  send_raw_message(msg.end_cell(), 64);
+
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() on_bounce (slice in_msg_body) impure {
+  in_msg_body~skip_bits(32); ;; 0xFFFFFFFF
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int op = in_msg_body~load_uint(32);
+  throw_unless(709, (op == op_internal_transfer) | (op == op_burn_notification));
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  balance += jetton_amount;
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+  if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+    return ();
+  }
+
+  slice cs = in_msg_full.begin_parse();
+  int flags = cs~load_uint(4);
+  if (flags & 1) {
+    on_bounce(in_msg_body);
+    return ();
+  }
+  slice sender_address = cs~load_msg_addr();
+  cs~load_msg_addr(); ;; skip dst
+  cs~load_coins(); ;; skip value
+  cs~skip_bits(1); ;; skip extracurrency collection
+  cs~load_coins(); ;; skip ihr_fee
+  int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs
+
+  int op = in_msg_body~load_uint(32);
+
+  if (op == op_transfer) { ;; outgoing transfer
+    send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
+    return ();
+  }
+
+  if (op == op_internal_transfer) { ;; incoming transfer
+    receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
+    return ();
+  }
+
+  if (op == op_burn) { ;; burn
+    burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
+    return ();
+  }
+
+  throw(0xffff);
+}
+
+(int, slice, slice, cell) get_wallet_data() method_id {
+  return load_data();
+}
diff --git a/src/toncli/projects/jetton_minter/func/op-codes.func b/src/toncli/projects/jetton_minter/func/op-codes.func
new file mode 100644
index 0000000..2b6ff80
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/op-codes.func
@@ -0,0 +1,9 @@
+const int op_transfer = 0xf8a7ea5;
+const int op_transfer_notification = 0x7362d09c;
+const int op_internal_transfer = 0x178d4519;
+const int op_excesses = 0xd53276db;
+const int op_burn = 0x595f07bc;
+const int op_burn_notification = 0x7bdd97de;
+
+;; Minter
+const int op_mint = 21;
diff --git a/src/toncli/projects/jetton_minter/func/params.func b/src/toncli/projects/jetton_minter/func/params.func
new file mode 100644
index 0000000..eaaaa27
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/func/params.func
@@ -0,0 +1,6 @@
+const int workchain = 0;
+
+() force_chain(slice addr) impure {
+  (int wc, _) = parse_std_addr(addr);
+  throw_unless(333, wc == workchain);
+}
diff --git a/src/toncli/projects/jetton_minter/project.yaml b/src/toncli/projects/jetton_minter/project.yaml
new file mode 100644
index 0000000..8a0afd9
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/project.yaml
@@ -0,0 +1,23 @@
+jetton_minter:
+  data: fift/minter_data.fif
+  func:
+    - func/op-codes.func
+    - func/params.func
+    - func/helpers.func
+    - func/jetton-utils.func
+    - func/jetton-minter.func
+  tests:
+    - tests/minter-tests.func
+    - tests/minter-tests-int.func
+
+jetton_wallet:
+  data: fift/wallet_data.fif
+  func:
+    - func/op-codes.func
+    - func/params.func
+    - func/helpers.func
+    - func/jetton-utils.func
+    - func/jetton-wallet.func
+  tests:
+    - tests/wallet-tests.func
+    - tests/wallet-tests-int.func
diff --git a/src/toncli/projects/jetton_wallet/.gitignore b/src/toncli/projects/jetton_wallet/.gitignore
new file mode 100644
index 0000000..378b0b4
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/.gitignore
@@ -0,0 +1,1 @@
+*.pk
diff --git a/src/toncli/projects/jetton_wallet/build/.gitkeep b/src/toncli/projects/jetton_wallet/build/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_wallet/build/boc/.gitkeep b/src/toncli/projects/jetton_wallet/build/boc/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_wallet/fift/.gitkeep b/src/toncli/projects/jetton_wallet/fift/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_wallet/fift/burn_jettons.fif b/src/toncli/projects/jetton_wallet/fift/burn_jettons.fif
new file mode 100644
index 0000000..bfd6c35
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/fift/burn_jettons.fif
@@ -0,0 +1,8 @@
+"TonUtil.fif" include
+
+<b
+  0x595f07bc 32 u, // op::burn
+  12345 64 u, // query_id
+  5000000000 Gram, // Burning 5 jettons
+  0 3 u, // addr_none for resp_dst and no custom payload
+b>
diff --git a/src/toncli/projects/jetton_wallet/fift/send_jettons.fif b/src/toncli/projects/jetton_wallet/fift/send_jettons.fif
new file mode 100644
index 0000000..9574f61
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/fift/send_jettons.fif
@@ -0,0 +1,27 @@
+"TonUtil.fif" include
+
+"EQDRebAnF1pvH1YsKNp7mtpsz+CLs6WxaffUojt1ijyrazkg" constant receiver_address
+1000000000 constant send_amount
+20000000 constant forward_amount
+12345 constant query_id
+0xf8a7ea5 constant op_transfer
+
+receiver_address
+$>smca 0= abort"Specify valid send addr"
+drop  // Drop flags
+
+2constant send_addr // worchain and addr into single constant
+<b
+  op_transfer 32 u, // op transfer
+  query_id 64 u, // query_id
+  send_amount Gram, // Sending one jetton
+  send_addr Addr,
+  0 2 u, // addr_none for resp_dst
+  0 1 u, // No custom payload
+  forward_amount Gram, // forward ton amount 0.03
+  0 1 u, // no ref forward_payload
+b>
+// Here goes master message
+// internal_transfer
+
+
diff --git a/src/toncli/projects/jetton_wallet/fift/wallet_data.fif b/src/toncli/projects/jetton_wallet/fift/wallet_data.fif
new file mode 100644
index 0000000..64abbf5
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/fift/wallet_data.fif
@@ -0,0 +1,30 @@
+"TonUtil.fif" include
+"Asm.fif" include
+
+1000000000 10 * constant initial_balance // Wallet balance 
+"build/jetton_wallet.fif" constant wallet_code
+
+/* 
+ This file is primerely for tests to run
+ Main way of deploying jetton wallet is via jetton minter "mint" operation.
+ Sending jettons from already deployed wallet is also an option
+ Thus random 256 bit value will be used ans and address for collection and owner
+
+ We're going to use newkeypair as a way to generate two random 256 bit values
+ Do not do that in real app 
+*/
+
+newkeypair 
+
+256 B>u@
+swap
+256 B>u@
+
+<b
+  initial_balance Gram, // 10 jettons balance ( if decimals is set to 9 )
+  Basechain rot Addr, // owner
+  Basechain rot Addr, // minter addr
+  wallet_code include
+  over tuple? { nip } if // In testing mode include will produce tuple with method names we don't want that in the cell 
+  ref, // wallet code
+b>
diff --git a/src/toncli/projects/jetton_wallet/func/helpers.func b/src/toncli/projects/jetton_wallet/func/helpers.func
new file mode 100644
index 0000000..8cf2360
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/func/helpers.func
@@ -0,0 +1,2 @@
+int equal_slices (slice a, slice b) asm "SDEQ";
+int tuple_length( tuple t ) asm "TLEN";
diff --git a/src/toncli/projects/jetton_wallet/func/jetton-utils.func b/src/toncli/projects/jetton_wallet/func/jetton-utils.func
new file mode 100644
index 0000000..fda1c8d
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/func/jetton-utils.func
@@ -0,0 +1,34 @@
+#pragma version >=0.2.0;
+#include "params.func";
+
+
+cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+   return  begin_cell()
+            .store_coins(balance)
+            .store_slice(owner_address)
+            .store_slice(jetton_master_address)
+            .store_ref(jetton_wallet_code)
+           .end_cell();
+}
+
+cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+  return begin_cell()
+          .store_uint(0, 2)
+          .store_dict(jetton_wallet_code)
+          .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code))
+          .store_uint(0, 1)
+         .end_cell();
+}
+
+slice calculate_jetton_wallet_address(cell state_init) inline {
+  return begin_cell().store_uint(4, 3)
+                     .store_int(workchain, 8)
+                     .store_uint(cell_hash(state_init), 256)
+                     .end_cell()
+                     .begin_parse();
+}
+
+slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline {
+  return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code));
+}
+
diff --git a/src/toncli/projects/jetton_wallet/func/jetton-wallet.func b/src/toncli/projects/jetton_wallet/func/jetton-wallet.func
new file mode 100644
index 0000000..3e82459
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/func/jetton-wallet.func
@@ -0,0 +1,251 @@
+;; Jetton Wallet Smart Contract
+
+{-
+
+NOTE that this tokens can be transferred within the same workchain.
+
+This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions:
+
+1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`)
+
+2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain)
+
+-}
+
+#pragma version >=0.2.0;
+#include "op-codes.func";
+#include "params.func";
+#include "helpers.func";
+#include "jetton-utils.func";
+
+
+
+
+const int min_tons_for_storage = 10000000; ;; 0.01 TON
+const int gas_consumption = 10000000; ;; 0.01 TON
+
+{-
+  Storage
+  storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage;
+-}
+
+(int, slice, slice, cell) load_data() inline {
+  slice ds = get_data().begin_parse();
+  return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
+}
+
+() save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline {
+  set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code));
+}
+
+{-
+  transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
+           response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+           forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+           = InternalMsgBody;
+  internal_transfer  query_id:uint64 amount:(VarUInteger 16) from:MsgAddress
+                     response_address:MsgAddress
+                     forward_ton_amount:(VarUInteger 16)
+                     forward_payload:(Either Cell ^Cell) 
+                     = InternalMsgBody;
+-}
+
+() send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  slice to_owner_address = in_msg_body~load_msg_addr();
+  force_chain(to_owner_address);
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  balance -= jetton_amount;
+
+  throw_unless(705, equal_slices(owner_address, sender_address));
+  throw_unless(706, balance >= 0);
+
+  cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code);
+  slice to_wallet_address = calculate_jetton_wallet_address(state_init);
+  slice response_address = in_msg_body~load_msg_addr();
+  cell custom_payload = in_msg_body~load_dict();
+  int forward_ton_amount = in_msg_body~load_coins();
+  throw_unless(708, slice_bits(in_msg_body) >= 1);
+  slice either_forward_payload = in_msg_body;
+  var msg = begin_cell()
+    .store_uint(0x18, 6)
+    .store_slice(to_wallet_address)
+    .store_coins(0)
+    .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
+    .store_ref(state_init);
+  var msg_body = begin_cell()
+    .store_uint(op_internal_transfer, 32)
+    .store_uint(query_id, 64)
+    .store_coins(jetton_amount)
+    .store_slice(owner_address)
+    .store_slice(response_address)
+    .store_coins(forward_ton_amount)
+    .store_slice(either_forward_payload)
+    .end_cell();
+
+  msg = msg.store_ref(msg_body);
+  int fwd_count = forward_ton_amount ? 2 : 1;
+  throw_unless(709, msg_value >
+                     forward_ton_amount +
+                     ;; 3 messages: wal1->wal2,  wal2->owner, wal2->response
+                     ;; but last one is optional (it is ok if it fails)
+                     fwd_count * fwd_fee +
+                     (2 * gas_consumption + min_tons_for_storage));
+                     ;; universal message send fee calculation may be activated here
+                     ;; by using this instead of fwd_fee
+                     ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15)
+
+  send_raw_message(msg.end_cell(), 64); ;; revert on errors
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+{-
+  internal_transfer  query_id:uint64 amount:(VarUInteger 16) from:MsgAddress
+                     response_address:MsgAddress
+                     forward_ton_amount:(VarUInteger 16)
+                     forward_payload:(Either Cell ^Cell) 
+                     = InternalMsgBody;
+-}
+
+() receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure {
+  ;; NOTE we can not allow fails in action phase since in that case there will be
+  ;; no bounce. Thus check and throw in computation phase.
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  balance += jetton_amount;
+  slice from_address = in_msg_body~load_msg_addr();
+  slice response_address = in_msg_body~load_msg_addr();
+  throw_unless(707,
+      equal_slices(jetton_master_address, sender_address)
+      |
+      equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address)
+  );
+  int forward_ton_amount = in_msg_body~load_coins();
+
+  int ton_balance_before_msg = my_ton_balance - msg_value;
+  int storage_fee = min_tons_for_storage - min(ton_balance_before_msg, min_tons_for_storage);
+  msg_value -= (storage_fee + gas_consumption);
+  if(forward_ton_amount) {
+    msg_value -= (forward_ton_amount + fwd_fee);
+    slice either_forward_payload = in_msg_body;
+
+    var msg_body = begin_cell()
+        .store_uint(op_transfer_notification, 32)
+        .store_uint(query_id, 64)
+        .store_coins(jetton_amount)
+        .store_slice(from_address)
+        .store_slice(either_forward_payload)
+        .end_cell();
+
+    var msg = begin_cell()
+      .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract
+      .store_slice(owner_address)
+      .store_coins(forward_ton_amount)
+      .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+      .store_ref(msg_body);
+
+    send_raw_message(msg.end_cell(), 1);
+  }
+
+  if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) {
+    var msg = begin_cell()
+      .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
+      .store_slice(response_address)
+      .store_coins(msg_value)
+      .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+      .store_uint(op_excesses, 32)
+      .store_uint(query_id, 64);
+    send_raw_message(msg.end_cell(), 2);
+  }
+
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure {
+  ;; NOTE we can not allow fails in action phase since in that case there will be
+  ;; no bounce. Thus check and throw in computation phase.
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  slice response_address = in_msg_body~load_msg_addr();
+  ;; ignore custom payload
+  ;; slice custom_payload = in_msg_body~load_dict();
+  balance -= jetton_amount;
+  throw_unless(705, equal_slices(owner_address, sender_address));
+  throw_unless(706, balance >= 0);
+  throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption);
+
+  var msg_body = begin_cell()
+      .store_uint(op_burn_notification, 32)
+      .store_uint(query_id, 64)
+      .store_coins(jetton_amount)
+      .store_slice(owner_address)
+      .store_slice(response_address)
+      .end_cell();
+
+  var msg = begin_cell()
+    .store_uint(0x18, 6)
+    .store_slice(jetton_master_address)
+    .store_coins(0)
+    .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+    .store_ref(msg_body);
+
+  send_raw_message(msg.end_cell(), 64);
+
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() on_bounce (slice in_msg_body) impure {
+  in_msg_body~skip_bits(32); ;; 0xFFFFFFFF
+  (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data();
+  int op = in_msg_body~load_uint(32);
+  throw_unless(709, (op == op_internal_transfer) | (op == op_burn_notification));
+  int query_id = in_msg_body~load_uint(64);
+  int jetton_amount = in_msg_body~load_coins();
+  balance += jetton_amount;
+  save_data(balance, owner_address, jetton_master_address, jetton_wallet_code);
+}
+
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+  if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+    return ();
+  }
+
+  slice cs = in_msg_full.begin_parse();
+  int flags = cs~load_uint(4);
+  if (flags & 1) {
+    on_bounce(in_msg_body);
+    return ();
+  }
+  slice sender_address = cs~load_msg_addr();
+  cs~load_msg_addr(); ;; skip dst
+  cs~load_coins(); ;; skip value
+  cs~skip_bits(1); ;; skip extracurrency collection
+  cs~load_coins(); ;; skip ihr_fee
+  int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs
+
+  int op = in_msg_body~load_uint(32);
+
+  if (op == op_transfer) { ;; outgoing transfer
+    send_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
+    return ();
+  }
+
+  if (op == op_internal_transfer) { ;; incoming transfer
+    receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value);
+    return ();
+  }
+
+  if (op == op_burn) { ;; burn
+    burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee);
+    return ();
+  }
+
+  throw(0xffff);
+}
+
+(int, slice, slice, cell) get_wallet_data() method_id {
+  return load_data();
+}
diff --git a/src/toncli/projects/jetton_wallet/func/op-codes.func b/src/toncli/projects/jetton_wallet/func/op-codes.func
new file mode 100644
index 0000000..2b6ff80
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/func/op-codes.func
@@ -0,0 +1,9 @@
+const int op_transfer = 0xf8a7ea5;
+const int op_transfer_notification = 0x7362d09c;
+const int op_internal_transfer = 0x178d4519;
+const int op_excesses = 0xd53276db;
+const int op_burn = 0x595f07bc;
+const int op_burn_notification = 0x7bdd97de;
+
+;; Minter
+const int op_mint = 21;
diff --git a/src/toncli/projects/jetton_wallet/func/params.func b/src/toncli/projects/jetton_wallet/func/params.func
new file mode 100644
index 0000000..eaaaa27
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/func/params.func
@@ -0,0 +1,6 @@
+const int workchain = 0;
+
+() force_chain(slice addr) impure {
+  (int wc, _) = parse_std_addr(addr);
+  throw_unless(333, wc == workchain);
+}
diff --git a/src/toncli/projects/jetton_wallet/project.yaml b/src/toncli/projects/jetton_wallet/project.yaml
new file mode 100644
index 0000000..4eb8607
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/project.yaml
@@ -0,0 +1,11 @@
+jetton_wallet:
+  data: fift/wallet_data.fif
+  func:
+    - func/helpers.func
+    - func/op-codes.func
+    - func/params.func
+    - func/jetton-utils.func
+    - func/jetton-wallet.func
+  tests:
+    - tests/wallet-tests.func
+    - tests/wallet-tests-int.func
diff --git a/src/toncli/projects/nft_collection/.gitignore b/src/toncli/projects/nft_collection/.gitignore
new file mode 100644
index 0000000..378b0b4
--- /dev/null
+++ b/src/toncli/projects/nft_collection/.gitignore
@@ -0,0 +1,1 @@
+*.pk
diff --git a/src/toncli/projects/nft_collection/README.md b/src/toncli/projects/nft_collection/README.md
new file mode 100644
index 0000000..27efa70
--- /dev/null
+++ b/src/toncli/projects/nft_collection/README.md
@@ -0,0 +1,55 @@
+# NFT Collection example project
+
+This project allows you to:
+
+1.  Build basic nft collection contract
+2.  Aims to *hopefully* test any nft collection contract for compliance with [NFT Standard](https://github.com/ton-blockchain/TIPs/issues/62)
+3.  Deploy collection contract via `toncli deploy nft_collection`
+4.  Manually deploy NFT item to the collection look (Deploying individual items)
+
+## Building
+
+  Just run `toncli build`
+  Depending on your fift/func build you may want
+  to uncomment some *func/helpers*
+
+## Testing
+
+  Build project and then: `toncli run_test`  
+
+  ⚠ If you see `6` error code on all tests - you need to update your binary [more information here](https://github.com/disintar/toncli/issues/72)
+  
+## Deploying collection contract
+
+  This project consists of two subprojects **nft_item** and **nft_collection**
+  You can see that in the *project.yml*
+  **BOTH** of those have to be built.
+  However, it makes sense to deploy only *nft_collection*.  
+  Prior to deployment you need to check out *fift/collection-data.fif*
+  and change all mock configuration values like collection_content,
+  owner_address Etc.  
+  To deploy run:`toncli deploy -n testnet nft_collection`.  
+  
+## Deploying individual items
+
+  To deploy your own NFT item to the already deployed collection
+  you will need:  
+  
++   Configure *fift/deploy.fif* script with your own values:
+[Take a look](https://github.com/ton-blockchain/TIPs/issues/64)  
+
++   Make yourself familiar with process of sending  [internal messages](https://github.com/disintar/toncli/blob/master/docs/advanced/send_fift_internal.md)  
+
+`toncli send -n testnet -a 0.05 -c nft_collection --body fift/deploy.fif`  
+Every next item deployment you should make sure to
+change item index in the *fift/deploy.fif* file ( Yes. Manually for now ).
+
+## Parse nft content
+
+Parse nft for collection (will work only if collection-data is same with on-chain):
+
+`toncli get get_nft_data -a "NFT_ADDRESS" --fift ./fift/parse-data-nft-collection.fif`
+
+Parse nft for single:
+
+`toncli get get_nft_data -a "NFT_ADDRESS" --fift ./fift/parse-data-nft-single.fif`
diff --git a/src/toncli/projects/nft_collection/build/.gitkeep b/src/toncli/projects/nft_collection/build/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_collection/build/boc/.gitkeep b/src/toncli/projects/nft_collection/build/boc/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_collection/fift/.gitkeep b/src/toncli/projects/nft_collection/fift/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_collection/fift/collection-data.fif b/src/toncli/projects/nft_collection/fift/collection-data.fif
new file mode 100644
index 0000000..d624048
--- /dev/null
+++ b/src/toncli/projects/nft_collection/fift/collection-data.fif
@@ -0,0 +1,68 @@
+"TonUtil.fif" include
+"Color.fif" include
+"Asm.fif" include
+
+// toncli creates file in fift-libs
+// where store deploy wallet address
+// you can use this address to control nft or nft collection
+"OwnerAddr.fif" include
+
+// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" instead of owner_address <- you can use address in any form
+// or use address of toncli deploy wallet:
+owner_address constant collection_owner_address
+
+// Change to your own
+"EQB8EqvPWlUq6g3pZPW9AQU4xLDDh65MxfFT_UStljYuCy_X" constant royalty_address
+
+// 5 / 100 = 5% :)
+5 constant royalty_numerator
+100 constant royalty_denominator
+
+// Specify your own collection base uri
+"https://raw.githubusercontent.com/Trinketer22/token-contract/main/nft/web-example/" constant collection_base
+
+// Specify your own collection metadata uri
+"https://raw.githubusercontent.com/Trinketer22/token-contract/main/nft/web-example/my_collection.json" constant collection_json
+
+
+// Path to builded NFT smart contract
+"build/nft.fif" constant nft_path
+
+// Parse and save addresses
+
+royalty_address $>smca 0= abort"Specify valid royalty addr" drop
+2constant royalty_raw
+
+collection_owner_address $>smca 0= abort"Specify valid owner addr" drop
+2constant owner_raw
+
+^reset ."👽 Owner Address: "  owner_address ^green type cr
+^reset ."👻 Royalty Address: " royalty_address ^green type cr
+^reset
+
+nft_path include constant nft_code
+
+// just little helper
+{ <b 1 8 u, swap $>B B, b> } : offchain-token-data
+
+
+// This is final c4 of smart contract
+// It will be loaded in nft-collection.func in 11 line
+
+<b
+  owner_raw Addr, // owner address
+  0 64 u, // start nft index
+
+  <b
+    collection_base offchain-token-data ref, // Storing collection base data ref
+    collection_json offchain-token-data ref, // Storing common ref
+  b> ref, // content cell
+
+  nft_code ref, // nft code
+
+  <b
+    royalty_numerator 16 u,
+    royalty_denominator 16 u,
+    royalty_raw Addr,
+  b> ref, // royalty cell
+b>
\ No newline at end of file
diff --git a/src/toncli/projects/nft_collection/fift/deploy.fif b/src/toncli/projects/nft_collection/fift/deploy.fif
new file mode 100644
index 0000000..bdde92f
--- /dev/null
+++ b/src/toncli/projects/nft_collection/fift/deploy.fif
@@ -0,0 +1,27 @@
+"TonUtil.fif" include
+
+50000000 =: storage_grams
+
+// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP"
+"TotallyNotValid" constant nft_owner_address
+
+."Deploying with nft owner address:" nft_owner_address type cr
+nft_owner_address $>smca 0= abort"Specify valid owner address"
+drop // Dropping address flags
+
+<b
+  -rot Addr, // nft owner address
+  <b "my_nft.json" $, b> ref, // nft content
+b> constant nft_payload
+
+<b
+  1 32 u, // op deploy_nft
+  12345 64 u, // query_id
+  0 64 u, // nft_idx b>
+  storage_grams Gram, // Storing forward_amount for nft deployment
+  // Initialization message body consists of owner_address and ref to the nft_content cell
+  nft_payload ref, // Storing content ref
+b>
+
+
+
diff --git a/src/toncli/projects/nft_collection/fift/nft-data.fif b/src/toncli/projects/nft_collection/fift/nft-data.fif
new file mode 100644
index 0000000..1280606
--- /dev/null
+++ b/src/toncli/projects/nft_collection/fift/nft-data.fif
@@ -0,0 +1,39 @@
+"TonUtil.fif" include
+"Asm.fif" include
+
+"kQDzy14OkWffhTvu289ftEY0xFZCqNU1cUbpiv8rZvQviL5F" constant collection_address // Specify your own
+"EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" constant owner_address // Specify your own
+"my_nft.json" constant nft_json // Your nft metadata relative path to collection_base
+
+
+
+collection_address
+$>smca 0= abort"Specify valid collection address" 
+drop 
+
+2constant coll_raw
+
+."Collection address:" collection_address type cr
+
+owner_address
+$>smca 0= abort"Specify valid owner address"
+drop
+
+2constant owner_raw
+
+."Owner address:" owner_address type cr
+
+
+
+<b
+  1 8 u, // Needed only for collection-less ( owner_raw == addr_none )
+  nft_json
+  $>B B,
+b>
+
+<b
+  0 64 u, // item_idx
+  coll_raw Addr,
+  owner_raw Addr,
+  swap ref,
+b>
diff --git a/src/toncli/projects/nft_collection/fift/parse-data-nft-collection.fif b/src/toncli/projects/nft_collection/fift/parse-data-nft-collection.fif
new file mode 100644
index 0000000..db804a5
--- /dev/null
+++ b/src/toncli/projects/nft_collection/fift/parse-data-nft-collection.fif
@@ -0,0 +1,60 @@
+"TonUtil.fif" include
+"Color.fif" include
+
+// In stack: init?, index,  collection address, owner_address, body
+4 roll // In stack: index,  collection address, owner_address, body, init?
+^reset ."👋 NFT is inited: " ^magenta (dump) type cr
+
+3 roll // In stack: collection address, owner_address, body, index
+constant index
+^reset ."👿 NFT index: " ^green index (dump) type cr
+
+
+^reset 2 roll addr@ ."🤗 Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE
+constant nft-body
+^reset ."🤯 NFT Body: " nft-body <s ^cyan (dump) type cr //  In stack: owner_address, collection address SLICE, body SLICE
+^reset ."👻 NFT owner: "  addr@ -rot 2constant nft_owner_address nft_owner_address rot ^green  print-addr cr //  In stack: collection address SLICE, body SLICE, owner_address SLICE
+
+nft-body <s dup sbits 8 / B@ constant nft-data
+
+^reset ."😏 NFT individual content: "  nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content parsed: " nft-data B>$ (dump) type cr
+
+
+// Parse data with collection
+
+"../build/nft_collection.fif" include constant collection-code
+"./collection-data.fif" include constant collection-data
+
+0x076ef1ea           // magic
+0                    // actions
+0                    // msgs_sent
+0           // unixtime
+1                    // block_lt
+0       // trans_lt
+239                  // randseed
+1000000000 null pair // balance_remaining
+<b nft_owner_address Addr, b> <s   // myself
+null            // global_config
+10 tuple 1 tuple constant c7 // save as c7 cell
+
+index
+nft-body
+68445 // function selector (get_nft_content)
+collection-code <s // load func code as slice
+collection-data // add c4 cell lodaded from lite-client
+c7 // add c7 cell
+0x35 runvmx // Runvm, in stack now seqno
+2drop drop
+<s
+^reset ."🤪 NFT type tag: " 8 u@+ swap  ^green (dump) type cr
+
+^reset ."😏 NFT collection base content: " dup sbits 8 / B@+ swap dup constant collection-nft-data ^cyan  (dump) type cr
+^reset ."😏 NFT collection base content: " collection-nft-data B>$ ^cyan  (dump) type cr
+
+.s
+^reset ."😏 NFT individual content: " ref@ <s dup sbits 8 / B@ dup constant nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content: " nft-data B>$ ^green  (dump) type cr
+
+^reset ."🐾 NFT data: "  <b collection-nft-data  B, nft-data B, b> <s dup sbits 8 / B@ ^cyan  (dump) type cr
+^reset ."🐾 NFT data: "  <b collection-nft-data  B, nft-data B, b> <s dup sbits 8 / B@ B>$ ^cyan (dump) type cr
diff --git a/src/toncli/projects/nft_collection/fift/parse-data-nft-single.fif b/src/toncli/projects/nft_collection/fift/parse-data-nft-single.fif
new file mode 100644
index 0000000..cf0bbba
--- /dev/null
+++ b/src/toncli/projects/nft_collection/fift/parse-data-nft-single.fif
@@ -0,0 +1,22 @@
+"TonUtil.fif" include
+"Color.fif" include
+
+// In stack: init?, index,  collection address, owner_address, body
+4 roll // In stack: index,  collection address, owner_address, body, init?
+^reset ."👋 NFT is inited: " ^magenta (dump) type cr
+
+3 roll // In stack: collection address, owner_address, body, index
+constant index
+^reset ."👿 NFT index: " ^green index (dump) type cr
+
+
+^reset 2 roll addr@ ."🤗 Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE
+constant nft-body
+^reset ."🤯 NFT Body: " nft-body <s ^cyan (dump) type cr //  In stack: owner_address, collection address SLICE, body SLICE
+^reset ."👻 NFT owner: "  addr@ ^green  print-addr cr //  In stack: collection address SLICE, body SLICE, owner_address SLICE
+
+nft-body <s
+^reset ."🤪 NFT type tag: " 8 u@+ swap  ^green (dump) type cr
+^reset ."😏 NFT individual content: " dup sbits 8 / B@ dup constant nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content parsed: " nft-data B>$ (dump) type cr
+
diff --git a/src/toncli/projects/nft_collection/func/nft-collection.func b/src/toncli/projects/nft_collection/func/nft-collection.func
new file mode 100644
index 0000000..333509e
--- /dev/null
+++ b/src/toncli/projects/nft_collection/func/nft-collection.func
@@ -0,0 +1,170 @@
+;; NFT collection smart contract
+
+;; storage scheme
+;; default#_ royalty_factor:uint16 royalty_base:uint16 royalty_address:MsgAddress = RoyaltyParams;
+;; storage#_ owner_address:MsgAddress next_item_index:uint64
+;;           ^[collection_content:^Cell common_content:^Cell]
+;;           nft_item_code:^Cell
+;;           royalty_params:^RoyaltyParams
+;;           = Storage;
+
+#pragma version >=0.2.0;
+#include "utils/helpers.func";
+#include "utils/params.func";
+#include "utils/op-codes.func";
+
+(slice, int, cell, cell, cell) load_data() inline {
+  var ds = get_data().begin_parse();
+  return 
+    (ds~load_msg_addr(), ;; owner_address
+     ds~load_uint(64), ;; next_item_index
+     ds~load_ref(), ;; content
+     ds~load_ref(), ;; nft_item_code
+     ds~load_ref()  ;; royalty_params
+     );
+}
+
+() save_data(slice owner_address, int next_item_index, cell content, cell nft_item_code, cell royalty_params) impure inline {
+  set_data(begin_cell()
+    .store_slice(owner_address)
+    .store_uint(next_item_index, 64)
+    .store_ref(content)
+    .store_ref(nft_item_code)
+    .store_ref(royalty_params)
+    .end_cell());
+}
+
+cell calculate_nft_item_state_init(int item_index, cell nft_item_code) {
+  cell data = begin_cell().store_uint(item_index, 64).store_slice(my_address()).end_cell();
+  return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell();
+}
+
+slice calculate_nft_item_address(int wc, cell state_init) {
+  return begin_cell().store_uint(4, 3)
+                     .store_int(wc, 8)
+                     .store_uint(cell_hash(state_init), 256)
+                     .end_cell()
+                     .begin_parse();
+}
+
+() deploy_nft_item(int item_index, cell nft_item_code, int amount, cell nft_content) impure {
+  cell state_init = calculate_nft_item_state_init(item_index, nft_item_code);
+  slice nft_address = calculate_nft_item_address(workchain, state_init);
+  var msg = begin_cell()
+            .store_uint(0x18, 6)
+            .store_slice(nft_address)
+            .store_coins(amount)
+            .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
+            .store_ref(state_init)
+            .store_ref(nft_content);
+  send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors
+}
+
+() send_royalty_params(slice to_address, int query_id, slice data) impure inline {
+  var msg = begin_cell()
+    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
+    .store_slice(to_address)
+    .store_coins(0)
+    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+    .store_uint(op_report_royalty_params, 32)
+    .store_uint(query_id, 64)
+    .store_slice(data);
+  send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message
+}
+
+() recv_internal(cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+    slice cs = in_msg_full.begin_parse();
+    int flags = cs~load_uint(4);
+    
+    if (flags & 1) { ;; ignore all bounced messages
+        return ();
+    }
+    slice sender_address = cs~load_msg_addr();
+
+    int op = in_msg_body~load_uint(32);
+    int query_id = in_msg_body~load_uint(64);
+
+    var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data();
+
+    if (op == op_get_royalty_params) {
+        send_royalty_params(sender_address, query_id, royalty_params.begin_parse());
+        return ();
+    }
+
+    throw_unless(401, equal_slices(sender_address, owner_address));
+    
+  
+    if (op == 1) { ;; deploy new nft
+      int item_index = in_msg_body~load_uint(64);
+      throw_unless(402, item_index <= next_item_index);
+      var is_last = item_index == next_item_index;
+      deploy_nft_item(item_index, nft_item_code, in_msg_body~load_coins(), in_msg_body~load_ref());
+      if (is_last) {
+        next_item_index += 1;
+        save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
+      }
+      return ();
+    }
+    if (op == 2) { ;; batch deploy of new nfts
+      int counter = 0;
+      cell deploy_list = in_msg_body~load_ref();
+      do {
+        var (item_index, item, f?) = deploy_list~udict::delete_get_min(64);
+        if (f?) {
+          counter += 1;
+          if (counter >= 250) { ;; Limit due to limits of action list size
+            throw(399);
+          }
+
+          throw_unless(403 + counter, item_index <= next_item_index);
+          deploy_nft_item(item_index, nft_item_code, item~load_coins(), item~load_ref());
+          if (item_index == next_item_index) {
+            next_item_index += 1;
+          }
+        }
+      } until ( ~ f?);
+      save_data(owner_address, next_item_index, content, nft_item_code, royalty_params);
+      return ();
+    }
+    if (op == 3) { ;; change owner
+      slice new_owner = in_msg_body~load_msg_addr();
+      save_data(new_owner, next_item_index, content, nft_item_code, royalty_params);
+      return ();
+    }
+    throw(0xffff);
+}
+
+;; Get methods
+
+(int, cell, slice) get_collection_data() method_id {
+  var (owner_address, next_item_index, content, _, _) = load_data();
+  slice cs = content.begin_parse();
+  return (next_item_index, cs~load_ref(), owner_address);
+}
+
+slice get_nft_address_by_index(int index) method_id {
+    var (_, _, _, nft_item_code, _) = load_data();
+    cell state_init = calculate_nft_item_state_init(index, nft_item_code);
+    return calculate_nft_item_address(workchain, state_init);
+}
+
+(int, int, slice) royalty_params() method_id {
+     var (_, _, _, _, royalty) = load_data();
+     slice rs = royalty.begin_parse();
+     return (rs~load_uint(16), rs~load_uint(16), rs~load_msg_addr());
+}
+
+cell get_nft_content(int index, cell individual_nft_content) method_id {
+  var (_, _, content, _, _) = load_data();
+  slice cs = content.begin_parse();
+  cs~load_ref();
+  slice common_content = cs~load_ref().begin_parse();
+  return (begin_cell()
+                      .store_uint(1, 8) ;; offchain tag
+                      .store_slice(common_content)
+                      .store_ref(individual_nft_content)
+          .end_cell());
+}
diff --git a/src/toncli/projects/nft_collection/func/nft-item.func b/src/toncli/projects/nft_collection/func/nft-item.func
new file mode 100644
index 0000000..18f7caf
--- /dev/null
+++ b/src/toncli/projects/nft_collection/func/nft-item.func
@@ -0,0 +1,151 @@
+;;
+;;  TON NFT Item Smart Contract
+;;
+
+{-
+
+    NOTE that this tokens can be transferred within the same workchain.
+
+    This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions:
+
+    1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`)
+
+    2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain)
+
+-}
+
+#pragma version >=0.2.0;
+#include "utils/helpers.func";
+#include "utils/params.func";
+#include "utils/op-codes.func";
+
+
+
+const int min_tons_for_storage = 50000000; ;; 0.05 TON
+
+;;
+;;  Storage
+;;
+;;  uint64 index
+;;  MsgAddressInt collection_address
+;;  MsgAddressInt owner_address
+;;  cell content
+;;
+
+(int, int, slice, slice, cell) load_data() {
+    slice ds = get_data().begin_parse();
+    var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr());
+    if (ds.slice_bits() > 0) {
+      return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref());
+    } else {  
+      return (0, index, collection_address, null(), null()); ;; nft not initialized yet
+    }
+}
+
+() store_data(int index, slice collection_address, slice owner_address, cell content) impure {
+    set_data(
+        begin_cell()
+            .store_uint(index, 64)
+            .store_slice(collection_address)
+            .store_slice(owner_address)
+            .store_ref(content)
+            .end_cell()
+    );
+}
+
+() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
+  var msg = begin_cell()
+    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
+    .store_slice(to_address)
+    .store_coins(amount)
+    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+    .store_uint(op, 32)
+    .store_uint(query_id, 64);
+
+  if (~ builder_null?(payload)) {
+    msg = msg.store_builder(payload);
+  }
+
+  send_raw_message(msg.end_cell(), send_mode);
+}
+
+() 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 {
+    throw_unless(401, equal_slices(sender_address, owner_address));
+
+    slice new_owner_address = in_msg_body~load_msg_addr();
+    force_chain(new_owner_address);
+    slice response_destination = in_msg_body~load_msg_addr();
+    in_msg_body~load_int(1); ;; this nft don't use custom_payload
+    int forward_amount = in_msg_body~load_coins();
+
+    int rest_amount = my_balance - min_tons_for_storage;
+    if (forward_amount) {
+      rest_amount -= (forward_amount + fwd_fees);
+    }
+    int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
+    if (need_response) {
+      rest_amount -= fwd_fees;
+    }
+
+    throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
+
+    if (forward_amount) {
+      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
+    }
+    if (need_response) {
+      force_chain(response_destination);
+      send_msg(response_destination, rest_amount, op_excesses, query_id, null(), 1); ;; paying fees, revert on errors
+    }
+
+    store_data(index, collection_address, new_owner_address, content);
+}
+
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+
+    slice cs = in_msg_full.begin_parse();
+    int flags = cs~load_uint(4);
+
+    if (flags & 1) { ;; ignore all bounced messages
+        return ();
+    }
+    slice sender_address = cs~load_msg_addr();
+
+    cs~load_msg_addr(); ;; skip dst
+    cs~load_coins(); ;; skip value
+    cs~skip_bits(1); ;; skip extracurrency collection
+    cs~load_coins(); ;; skip ihr_fee
+    int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs
+
+
+    (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
+    if (~ init?) {
+      throw_unless(405, equal_slices(collection_address, sender_address));
+      store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref());
+      return ();
+    }
+
+    int op = in_msg_body~load_uint(32);
+    int query_id = in_msg_body~load_uint(64);
+
+    if (op == op_transfer) {
+      transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee);
+      return ();
+    }
+    if (op == op_get_static_data) {
+      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
+      return ();
+    }
+    throw(0xffff);
+}
+
+;;
+;;  GET Methods
+;;
+
+(int, int, slice, slice, cell) get_nft_data() method_id {
+  (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
+  return (init?, index, collection_address, owner_address, content);
+}
diff --git a/src/toncli/projects/nft_collection/func/utils/helpers.func b/src/toncli/projects/nft_collection/func/utils/helpers.func
new file mode 100644
index 0000000..2dd37bc
--- /dev/null
+++ b/src/toncli/projects/nft_collection/func/utils/helpers.func
@@ -0,0 +1,222 @@
+;; Standard library for funC
+;;
+
+;;forall X -> tuple cons(X head, tuple tail) asm "CONS";
+;;forall X -> (X, tuple) uncons(tuple list) asm "UNCONS";
+;;forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS";
+;;forall X -> X car(tuple list) asm "CAR";
+;;tuple cdr(tuple list) asm "CDR";
+;;tuple empty_tuple() asm "NIL";
+;;forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
+;;forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
+;;forall X -> [X] single(X x) asm "SINGLE";
+;;forall X -> X unsingle([X] t) asm "UNSINGLE";
+;;forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR";
+;;forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR";
+;;forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE";
+;;forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE";
+;;forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE";
+;;forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE";
+;;forall X -> X first(tuple t) asm "FIRST";
+;;forall X -> X second(tuple t) asm "SECOND";
+;;forall X -> X third(tuple t) asm "THIRD";
+;;forall X -> X fourth(tuple t) asm "3 INDEX";
+;;forall X, Y -> X pair_first([X, Y] p) asm "FIRST";
+;;forall X, Y -> Y pair_second([X, Y] p) asm "SECOND";
+;;forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST";
+;;forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND";
+;;forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD";
+;;forall X -> X null() asm "PUSHNULL";
+;;forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP";
+;;
+;;int now() asm "NOW";
+;;slice my_address() asm "MYADDR";
+;;[int, cell] get_balance() asm "BALANCE";
+;;int cur_lt() asm "LTIME";
+;;int block_lt() asm "BLOCKLT";
+;;
+;;int cell_hash(cell c) asm "HASHCU";
+;;int slice_hash(slice s) asm "HASHSU";
+;;int string_hash(slice s) asm "SHA256U";
+;;
+;;int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU";
+;;int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS";
+;;
+;;(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
+;;(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE";
+;;(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
+;;(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
+;;
+;;;; () throw_if(int excno, int cond) impure asm "THROWARGIF";
+;;
+;;() dump_stack() impure asm "DUMPSTK";
+;;
+;;cell get_data() asm "c4 PUSH";
+;;() set_data(cell c) impure asm "c4 POP";
+;;cont get_c3() impure asm "c3 PUSH";
+;;() set_c3(cont c) impure asm "c3 POP";
+;;cont bless(slice s) impure asm "BLESS";
+;;
+;;() accept_message() impure asm "ACCEPT";
+;;() set_gas_limit(int limit) impure asm "SETGASLIMIT";
+;;() commit() impure asm "COMMIT";
+;;() buy_gas(int gram) impure asm "BUYGAS";
+;;
+;;int min(int x, int y) asm "MIN";
+;;int max(int x, int y) asm "MAX";
+;;(int, int) minmax(int x, int y) asm "MINMAX";
+;;int abs(int x) asm "ABS";
+;;
+;;slice begin_parse(cell c) asm "CTOS";
+;;() end_parse(slice s) impure asm "ENDS";
+;;(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF";
+;;cell preload_ref(slice s) asm "PLDREF";
+;;;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
+;;;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX";
+;;;; int preload_int(slice s, int len) asm "PLDIX";
+;;;; int preload_uint(slice s, int len) asm "PLDUX";
+;;;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX";
+;;;; slice preload_bits(slice s, int len) asm "PLDSLICEX";
+;;(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS";
+;;slice skip_bits(slice s, int len) asm "SDSKIPFIRST";
+;;(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST";
+;;slice first_bits(slice s, int len) asm "SDCUTFIRST";
+;;slice skip_last_bits(slice s, int len) asm "SDSKIPLAST";
+;;(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST";
+;;slice slice_last(slice s, int len) asm "SDCUTLAST";
+;;(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT";
+;;cell preload_dict(slice s) asm "PLDDICT";
+;;slice skip_dict(slice s) asm "SKIPDICT";
+;;
+;;(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF";
+;;cell preload_maybe_ref(slice s) asm "PLDOPTREF";
+;;builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF";
+;;
+;;int cell_depth(cell c) asm "CDEPTH";
+;;
+;;int slice_refs(slice s) asm "SREFS";
+;;int slice_bits(slice s) asm "SBITS";
+;;(int, int) slice_bits_refs(slice s) asm "SBITREFS";
+;;int slice_empty?(slice s) asm "SEMPTY";
+;;int slice_data_empty?(slice s) asm "SDEMPTY";
+;;int slice_refs_empty?(slice s) asm "SREMPTY";
+;;int slice_depth(slice s) asm "SDEPTH";
+;;
+;;int builder_refs(builder b) asm "BREFS";
+;;int builder_bits(builder b) asm "BBITS";
+;;int builder_depth(builder b) asm "BDEPTH";
+;;
+;;builder begin_cell() asm "NEWC";
+;;cell end_cell(builder b) asm "ENDC";
+;;builder store_ref(builder b, cell c) asm(c b) "STREF";
+;;;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX";
+;;;; builder store_int(builder b, int x, int len) asm(x b len) "STIX";
+;;builder store_slice(builder b, slice s) asm "STSLICER";
+;;builder store_grams(builder b, int x) asm "STGRAMS";
+;;builder store_dict(builder b, cell c) asm(c b) "STDICT";
+;;
+;;(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR";
+;;tuple parse_addr(slice s) asm "PARSEMSGADDR";
+;;(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR";
+;;(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR";
+;;
+;;cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
+;;(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
+;;cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
+;;(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
+;;cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF";
+;;(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF";
+;;(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF";
+;;(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF";
+;;(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF";
+;;(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL";
+;;(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL";
+;;(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT";
+;;(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT";
+;;(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
+;;(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
+;;(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
+;;(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
+;;cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
+;;(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
+;;cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
+;;(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
+;;cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
+;;(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
+;;(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD";
+;;(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE";
+;;(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD";
+;;(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE";
+;;cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
+;;(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
+;;cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
+;;(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
+;;cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
+;;(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
+;;(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB";
+;;(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB";
+;;(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB";
+;;(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB";
+;;(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
+;;(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
+;;(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2";
+;;(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2";
+;;(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2";
+;;(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2";
+;;(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2";
+;;cell new_dict() asm "NEWDICT";
+;;int dict_empty?(cell c) asm "DICTEMPTY";
+;;
+;;(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2";
+;;(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET";
+;;(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL";
+
+;;cell config_param(int x) asm "CONFIGOPTPARAM";
+;;int cell_null?(cell c) asm "ISNULL";
+;;
+;;() raw_reserve(int amount, int mode) impure asm "RAWRESERVE";
+;;() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX";
+;;() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG";
+;;() set_code(cell new_code) impure asm "SETCODE";
+;;
+;;int random() impure asm "RANDU256";
+;;int rand(int range) impure asm "RAND";
+;;int get_seed() impure asm "RANDSEED";
+;;int set_seed() impure asm "SETRAND";
+;;() randomize(int x) impure asm "ADDRAND";
+;;() randomize_lt() impure asm "LTIME" "ADDRAND";
+;;
+;;builder store_coins(builder b, int x) asm "STVARUINT16";
+;;(slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16";
+
+int equal_slices (slice a, slice b) asm "SDEQ";
+int builder_null?(builder b) asm "ISNULL";
+int tuple_length( tuple t ) asm "TLEN";
+forall X -> int is_null(X x) asm "ISNULL";
+forall X -> int is_int(X x) asm "<{ TRY:<{ 0 PUSHINT ADD DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_cell(X x) asm "<{ TRY:<{ CTOS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_slice(X x) asm "<{ TRY:<{ SBITS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_tuple(X x) asm "ISTUPLE";
+;;builder store_builder(builder to, builder from) asm "STBR";
+
diff --git a/src/toncli/projects/nft_collection/func/utils/op-codes.func b/src/toncli/projects/nft_collection/func/utils/op-codes.func
new file mode 100644
index 0000000..b3f8be8
--- /dev/null
+++ b/src/toncli/projects/nft_collection/func/utils/op-codes.func
@@ -0,0 +1,12 @@
+const int op_transfer = 0x5fcc3d14;
+const int op_ownership_assigned = 0x05138d91;
+const int op_excesses = 0xd53276db;
+const int op_get_static_data = 0x2fcb26a2;
+const int op_report_static_data = 0x8b771735;
+const int op_get_royalty_params = 0x693d3950;
+const int op_report_royalty_params = 0xa8cb00ad;
+
+;; NFTEditable
+const int op_edit_content = 0x1a0b9d51;
+const int op_transfer_editorship = 0x1c04412a;
+const int op_editorship_assigned = 0x511a4463;
diff --git a/src/toncli/projects/nft_collection/func/utils/params.func b/src/toncli/projects/nft_collection/func/utils/params.func
new file mode 100644
index 0000000..eaaaa27
--- /dev/null
+++ b/src/toncli/projects/nft_collection/func/utils/params.func
@@ -0,0 +1,6 @@
+const int workchain = 0;
+
+() force_chain(slice addr) impure {
+  (int wc, _) = parse_std_addr(addr);
+  throw_unless(333, wc == workchain);
+}
diff --git a/src/toncli/projects/nft_collection/project.yaml b/src/toncli/projects/nft_collection/project.yaml
new file mode 100644
index 0000000..303f18a
--- /dev/null
+++ b/src/toncli/projects/nft_collection/project.yaml
@@ -0,0 +1,21 @@
+nft_collection:
+  data: fift/collection-data.fif
+  func:
+    - func/utils/helpers.func
+    - func/utils/params.func
+    - func/utils/op-codes.func
+    - func/nft-collection.func
+  tests:
+    - tests/collection-tests.func
+    - tests/collection-tests-int.func
+
+nft:
+  data: fift/nft-data.fif
+  func:
+    - func/utils/helpers.func
+    - func/utils/params.func
+    - func/utils/op-codes.func
+    - func/nft-item.func
+  tests:
+    - tests/nft-tests.func
+    - tests/nft-tests-int.func
diff --git a/src/toncli/projects/nft_item/README.md b/src/toncli/projects/nft_item/README.md
new file mode 100644
index 0000000..58af2fe
--- /dev/null
+++ b/src/toncli/projects/nft_item/README.md
@@ -0,0 +1,40 @@
+# NFT Item example project
+
+This project allows you to:
+
+1.  Build basic nft item contract
+2.  Aims to *hopefully* test any nft item contract for compliance with [NFT Standard](https://github.com/ton-blockchain/TIPs/issues/62)
+3.  Deploy collection-less nft item contract via `toncli deploy`
+
+## Building
+
+  Just run `toncli build`
+  Depending on your fift/func build you may want
+  to uncomment some of the *func/helpers*
+
+## Testing
+  Build project and then: `toncli run_test`
+
+  ⚠ If you see `6` error code on all tests - you need to update your binary [more information here](https://github.com/disintar/toncli/issues/72)
+
+## Deploying item contract
+
+  To deploy collection-less nft you should edit
+  "fift/nft-data.fif.  
+  This project is aimed mostly at testing and development of your own
+  nft item contrat to be then used with *nft_collection*.  
+
+  However, deploying collection-less NFT is still possible.
+
+-   Set *coll_raw* address to *addr_none*.
+  `0 2 u,` instead of `coll_raw Addr,` would be the
+  simpest way to do it.  
+
+-   Set *nft_json* to the full url pointing to your item metadata json
+
+  If you aim to deploy item within collection please *use nft_collection project*.  
+  To deploy run:`toncli deploy`.  
+
+## Get nft data
+
+`toncli get get_nft_data --fift ./fift/parse-data-nft-single.fif`
diff --git a/src/toncli/projects/nft_item/build/.gitkeep b/src/toncli/projects/nft_item/build/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_item/build/boc/.gitkeep b/src/toncli/projects/nft_item/build/boc/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_item/fift/.gitkeep b/src/toncli/projects/nft_item/fift/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_item/fift/deploy.fif b/src/toncli/projects/nft_item/fift/deploy.fif
new file mode 100644
index 0000000..7dea9cf
--- /dev/null
+++ b/src/toncli/projects/nft_item/fift/deploy.fif
@@ -0,0 +1,35 @@
+"TonUtil.fif" include
+
+// toncli creates file in fift-libs
+// where store deploy wallet address
+// you can use this address to control nft or nft collection
+"OwnerAddr.fif" include
+
+50000000 =: storage_grams
+
+// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" <- you can use address in any form
+// or use address of toncli deploy wallet:
+owner_address constant nft_owner_address
+
+."Deploying with nft owner address:" nft_owner_address type cr
+
+nft_owner_address $>smca 0= abort"Specify valid owner address"
+drop // Dropping address flags
+
+// Initialization message body consists of owner_address and ref to the nft_content cell
+<b
+  -rot Addr, // owner address
+  <b "my_nft.json" $, b> ref, // nft content
+b>
+
+
+<b
+  1 32 u, // op deploy_nft
+  12345 64 u, // query_id
+  0 64 u, // nft_idx b>
+  storage_grams Gram, // Storing forward_amount for nft deployment
+  swap ref, // Storing content ref
+b>
+
+
+
diff --git a/src/toncli/projects/nft_item/fift/nft-data.fif b/src/toncli/projects/nft_item/fift/nft-data.fif
new file mode 100644
index 0000000..da70430
--- /dev/null
+++ b/src/toncli/projects/nft_item/fift/nft-data.fif
@@ -0,0 +1,27 @@
+"TonUtil.fif" include
+"Asm.fif" include
+
+// toncli creates file in fift-libs
+// where store deploy wallet address
+// you can use this address to control nft or nft collection
+"OwnerAddr.fif" include
+
+// "EQDlT07NpSh0uj-aSBkF2TRxOqR2nw0ErOQsA6TYakr1-FxP" instead of owner_address <- you can use address in any form
+// or use address of toncli deploy wallet:
+owner_address constant nft_owner_address
+
+"https://my-cool-uri.lol/🤫" constant nft_json // Your nft metadata path to nft
+
+owner_address $>smca 0= abort"Specify valid owner address" drop
+2constant owner_raw
+."Owner address:" owner_address type cr
+
+<b
+  0 64 u, // item_idx
+  0 2 u, // collection-less
+  owner_raw Addr,
+  <b
+    1 8 u, // off-chain tag
+    nft_json $>B B,
+  b> ref,
+b>
\ No newline at end of file
diff --git a/src/toncli/projects/nft_item/fift/parse-data-nft-collection.fif b/src/toncli/projects/nft_item/fift/parse-data-nft-collection.fif
new file mode 100644
index 0000000..db804a5
--- /dev/null
+++ b/src/toncli/projects/nft_item/fift/parse-data-nft-collection.fif
@@ -0,0 +1,60 @@
+"TonUtil.fif" include
+"Color.fif" include
+
+// In stack: init?, index,  collection address, owner_address, body
+4 roll // In stack: index,  collection address, owner_address, body, init?
+^reset ."👋 NFT is inited: " ^magenta (dump) type cr
+
+3 roll // In stack: collection address, owner_address, body, index
+constant index
+^reset ."👿 NFT index: " ^green index (dump) type cr
+
+
+^reset 2 roll addr@ ."🤗 Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE
+constant nft-body
+^reset ."🤯 NFT Body: " nft-body <s ^cyan (dump) type cr //  In stack: owner_address, collection address SLICE, body SLICE
+^reset ."👻 NFT owner: "  addr@ -rot 2constant nft_owner_address nft_owner_address rot ^green  print-addr cr //  In stack: collection address SLICE, body SLICE, owner_address SLICE
+
+nft-body <s dup sbits 8 / B@ constant nft-data
+
+^reset ."😏 NFT individual content: "  nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content parsed: " nft-data B>$ (dump) type cr
+
+
+// Parse data with collection
+
+"../build/nft_collection.fif" include constant collection-code
+"./collection-data.fif" include constant collection-data
+
+0x076ef1ea           // magic
+0                    // actions
+0                    // msgs_sent
+0           // unixtime
+1                    // block_lt
+0       // trans_lt
+239                  // randseed
+1000000000 null pair // balance_remaining
+<b nft_owner_address Addr, b> <s   // myself
+null            // global_config
+10 tuple 1 tuple constant c7 // save as c7 cell
+
+index
+nft-body
+68445 // function selector (get_nft_content)
+collection-code <s // load func code as slice
+collection-data // add c4 cell lodaded from lite-client
+c7 // add c7 cell
+0x35 runvmx // Runvm, in stack now seqno
+2drop drop
+<s
+^reset ."🤪 NFT type tag: " 8 u@+ swap  ^green (dump) type cr
+
+^reset ."😏 NFT collection base content: " dup sbits 8 / B@+ swap dup constant collection-nft-data ^cyan  (dump) type cr
+^reset ."😏 NFT collection base content: " collection-nft-data B>$ ^cyan  (dump) type cr
+
+.s
+^reset ."😏 NFT individual content: " ref@ <s dup sbits 8 / B@ dup constant nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content: " nft-data B>$ ^green  (dump) type cr
+
+^reset ."🐾 NFT data: "  <b collection-nft-data  B, nft-data B, b> <s dup sbits 8 / B@ ^cyan  (dump) type cr
+^reset ."🐾 NFT data: "  <b collection-nft-data  B, nft-data B, b> <s dup sbits 8 / B@ B>$ ^cyan (dump) type cr
diff --git a/src/toncli/projects/nft_item/fift/parse-data-nft-single.fif b/src/toncli/projects/nft_item/fift/parse-data-nft-single.fif
new file mode 100644
index 0000000..07e46a9
--- /dev/null
+++ b/src/toncli/projects/nft_item/fift/parse-data-nft-single.fif
@@ -0,0 +1,21 @@
+"TonUtil.fif" include
+"Color.fif" include
+
+// In stack: init?, index,  collection address, owner_address, body
+4 roll // In stack: index,  collection address, owner_address, body, init?
+^reset ."👋 NFT is inited: " ^magenta (dump) type cr
+
+3 roll // In stack: collection address, owner_address, body, index
+constant index
+^reset ."👿 NFT index: " ^green index (dump) type cr
+
+
+^reset 2 roll addr@ ."🤗 Collection address: " ^yellow print-addr cr // In stack: owner_address, body, collection address SLICE
+constant nft-body
+^reset ."🤯 NFT Body: " nft-body <s ^cyan (dump) type cr //  In stack: owner_address, collection address SLICE, body SLICE
+^reset ."👻 NFT owner: "  addr@ ^green  print-addr cr //  In stack: collection address SLICE, body SLICE, owner_address SLICE
+
+nft-body <s
+^reset ."🤪 NFT type tag: " 8 u@+ swap  ^green (dump) type cr
+^reset ."😏 NFT individual content: " dup sbits 8 / B@ dup constant nft-data ^green  (dump) type cr
+^reset ."😏 NFT individual content parsed: " nft-data B>$ (dump) type cr
diff --git a/src/toncli/projects/nft_item/func/nft-item.func b/src/toncli/projects/nft_item/func/nft-item.func
new file mode 100644
index 0000000..18f7caf
--- /dev/null
+++ b/src/toncli/projects/nft_item/func/nft-item.func
@@ -0,0 +1,151 @@
+;;
+;;  TON NFT Item Smart Contract
+;;
+
+{-
+
+    NOTE that this tokens can be transferred within the same workchain.
+
+    This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions:
+
+    1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`)
+
+    2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain)
+
+-}
+
+#pragma version >=0.2.0;
+#include "utils/helpers.func";
+#include "utils/params.func";
+#include "utils/op-codes.func";
+
+
+
+const int min_tons_for_storage = 50000000; ;; 0.05 TON
+
+;;
+;;  Storage
+;;
+;;  uint64 index
+;;  MsgAddressInt collection_address
+;;  MsgAddressInt owner_address
+;;  cell content
+;;
+
+(int, int, slice, slice, cell) load_data() {
+    slice ds = get_data().begin_parse();
+    var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr());
+    if (ds.slice_bits() > 0) {
+      return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref());
+    } else {  
+      return (0, index, collection_address, null(), null()); ;; nft not initialized yet
+    }
+}
+
+() store_data(int index, slice collection_address, slice owner_address, cell content) impure {
+    set_data(
+        begin_cell()
+            .store_uint(index, 64)
+            .store_slice(collection_address)
+            .store_slice(owner_address)
+            .store_ref(content)
+            .end_cell()
+    );
+}
+
+() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
+  var msg = begin_cell()
+    .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
+    .store_slice(to_address)
+    .store_coins(amount)
+    .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
+    .store_uint(op, 32)
+    .store_uint(query_id, 64);
+
+  if (~ builder_null?(payload)) {
+    msg = msg.store_builder(payload);
+  }
+
+  send_raw_message(msg.end_cell(), send_mode);
+}
+
+() 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 {
+    throw_unless(401, equal_slices(sender_address, owner_address));
+
+    slice new_owner_address = in_msg_body~load_msg_addr();
+    force_chain(new_owner_address);
+    slice response_destination = in_msg_body~load_msg_addr();
+    in_msg_body~load_int(1); ;; this nft don't use custom_payload
+    int forward_amount = in_msg_body~load_coins();
+
+    int rest_amount = my_balance - min_tons_for_storage;
+    if (forward_amount) {
+      rest_amount -= (forward_amount + fwd_fees);
+    }
+    int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
+    if (need_response) {
+      rest_amount -= fwd_fees;
+    }
+
+    throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
+
+    if (forward_amount) {
+      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
+    }
+    if (need_response) {
+      force_chain(response_destination);
+      send_msg(response_destination, rest_amount, op_excesses, query_id, null(), 1); ;; paying fees, revert on errors
+    }
+
+    store_data(index, collection_address, new_owner_address, content);
+}
+
+() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
+    if (in_msg_body.slice_empty?()) { ;; ignore empty messages
+        return ();
+    }
+
+    slice cs = in_msg_full.begin_parse();
+    int flags = cs~load_uint(4);
+
+    if (flags & 1) { ;; ignore all bounced messages
+        return ();
+    }
+    slice sender_address = cs~load_msg_addr();
+
+    cs~load_msg_addr(); ;; skip dst
+    cs~load_coins(); ;; skip value
+    cs~skip_bits(1); ;; skip extracurrency collection
+    cs~load_coins(); ;; skip ihr_fee
+    int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs
+
+
+    (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
+    if (~ init?) {
+      throw_unless(405, equal_slices(collection_address, sender_address));
+      store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref());
+      return ();
+    }
+
+    int op = in_msg_body~load_uint(32);
+    int query_id = in_msg_body~load_uint(64);
+
+    if (op == op_transfer) {
+      transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee);
+      return ();
+    }
+    if (op == op_get_static_data) {
+      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
+      return ();
+    }
+    throw(0xffff);
+}
+
+;;
+;;  GET Methods
+;;
+
+(int, int, slice, slice, cell) get_nft_data() method_id {
+  (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data();
+  return (init?, index, collection_address, owner_address, content);
+}
diff --git a/src/toncli/projects/nft_item/func/utils/helpers.func b/src/toncli/projects/nft_item/func/utils/helpers.func
new file mode 100644
index 0000000..2dd37bc
--- /dev/null
+++ b/src/toncli/projects/nft_item/func/utils/helpers.func
@@ -0,0 +1,222 @@
+;; Standard library for funC
+;;
+
+;;forall X -> tuple cons(X head, tuple tail) asm "CONS";
+;;forall X -> (X, tuple) uncons(tuple list) asm "UNCONS";
+;;forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS";
+;;forall X -> X car(tuple list) asm "CAR";
+;;tuple cdr(tuple list) asm "CDR";
+;;tuple empty_tuple() asm "NIL";
+;;forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
+;;forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
+;;forall X -> [X] single(X x) asm "SINGLE";
+;;forall X -> X unsingle([X] t) asm "UNSINGLE";
+;;forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR";
+;;forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR";
+;;forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE";
+;;forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE";
+;;forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE";
+;;forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE";
+;;forall X -> X first(tuple t) asm "FIRST";
+;;forall X -> X second(tuple t) asm "SECOND";
+;;forall X -> X third(tuple t) asm "THIRD";
+;;forall X -> X fourth(tuple t) asm "3 INDEX";
+;;forall X, Y -> X pair_first([X, Y] p) asm "FIRST";
+;;forall X, Y -> Y pair_second([X, Y] p) asm "SECOND";
+;;forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST";
+;;forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND";
+;;forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD";
+;;forall X -> X null() asm "PUSHNULL";
+;;forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP";
+;;
+;;int now() asm "NOW";
+;;slice my_address() asm "MYADDR";
+;;[int, cell] get_balance() asm "BALANCE";
+;;int cur_lt() asm "LTIME";
+;;int block_lt() asm "BLOCKLT";
+;;
+;;int cell_hash(cell c) asm "HASHCU";
+;;int slice_hash(slice s) asm "HASHSU";
+;;int string_hash(slice s) asm "SHA256U";
+;;
+;;int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU";
+;;int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS";
+;;
+;;(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
+;;(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE";
+;;(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
+;;(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
+;;
+;;;; () throw_if(int excno, int cond) impure asm "THROWARGIF";
+;;
+;;() dump_stack() impure asm "DUMPSTK";
+;;
+;;cell get_data() asm "c4 PUSH";
+;;() set_data(cell c) impure asm "c4 POP";
+;;cont get_c3() impure asm "c3 PUSH";
+;;() set_c3(cont c) impure asm "c3 POP";
+;;cont bless(slice s) impure asm "BLESS";
+;;
+;;() accept_message() impure asm "ACCEPT";
+;;() set_gas_limit(int limit) impure asm "SETGASLIMIT";
+;;() commit() impure asm "COMMIT";
+;;() buy_gas(int gram) impure asm "BUYGAS";
+;;
+;;int min(int x, int y) asm "MIN";
+;;int max(int x, int y) asm "MAX";
+;;(int, int) minmax(int x, int y) asm "MINMAX";
+;;int abs(int x) asm "ABS";
+;;
+;;slice begin_parse(cell c) asm "CTOS";
+;;() end_parse(slice s) impure asm "ENDS";
+;;(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF";
+;;cell preload_ref(slice s) asm "PLDREF";
+;;;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
+;;;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX";
+;;;; int preload_int(slice s, int len) asm "PLDIX";
+;;;; int preload_uint(slice s, int len) asm "PLDUX";
+;;;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX";
+;;;; slice preload_bits(slice s, int len) asm "PLDSLICEX";
+;;(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS";
+;;slice skip_bits(slice s, int len) asm "SDSKIPFIRST";
+;;(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST";
+;;slice first_bits(slice s, int len) asm "SDCUTFIRST";
+;;slice skip_last_bits(slice s, int len) asm "SDSKIPLAST";
+;;(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST";
+;;slice slice_last(slice s, int len) asm "SDCUTLAST";
+;;(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT";
+;;cell preload_dict(slice s) asm "PLDDICT";
+;;slice skip_dict(slice s) asm "SKIPDICT";
+;;
+;;(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF";
+;;cell preload_maybe_ref(slice s) asm "PLDOPTREF";
+;;builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF";
+;;
+;;int cell_depth(cell c) asm "CDEPTH";
+;;
+;;int slice_refs(slice s) asm "SREFS";
+;;int slice_bits(slice s) asm "SBITS";
+;;(int, int) slice_bits_refs(slice s) asm "SBITREFS";
+;;int slice_empty?(slice s) asm "SEMPTY";
+;;int slice_data_empty?(slice s) asm "SDEMPTY";
+;;int slice_refs_empty?(slice s) asm "SREMPTY";
+;;int slice_depth(slice s) asm "SDEPTH";
+;;
+;;int builder_refs(builder b) asm "BREFS";
+;;int builder_bits(builder b) asm "BBITS";
+;;int builder_depth(builder b) asm "BDEPTH";
+;;
+;;builder begin_cell() asm "NEWC";
+;;cell end_cell(builder b) asm "ENDC";
+;;builder store_ref(builder b, cell c) asm(c b) "STREF";
+;;;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX";
+;;;; builder store_int(builder b, int x, int len) asm(x b len) "STIX";
+;;builder store_slice(builder b, slice s) asm "STSLICER";
+;;builder store_grams(builder b, int x) asm "STGRAMS";
+;;builder store_dict(builder b, cell c) asm(c b) "STDICT";
+;;
+;;(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR";
+;;tuple parse_addr(slice s) asm "PARSEMSGADDR";
+;;(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR";
+;;(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR";
+;;
+;;cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
+;;(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
+;;cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
+;;(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
+;;cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF";
+;;(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF";
+;;(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF";
+;;(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF";
+;;(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF";
+;;(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL";
+;;(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL";
+;;(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT";
+;;(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT";
+;;(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
+;;(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
+;;(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
+;;(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
+;;cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
+;;(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
+;;cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
+;;(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
+;;cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
+;;(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
+;;(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD";
+;;(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE";
+;;(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD";
+;;(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE";
+;;cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
+;;(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
+;;cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
+;;(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
+;;cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
+;;(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
+;;(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB";
+;;(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB";
+;;(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB";
+;;(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB";
+;;(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
+;;(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
+;;(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
+;;(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
+;;(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
+;;(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2";
+;;(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2";
+;;(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2";
+;;(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2";
+;;(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2";
+;;(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2";
+;;(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2";
+;;cell new_dict() asm "NEWDICT";
+;;int dict_empty?(cell c) asm "DICTEMPTY";
+;;
+;;(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2";
+;;(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET";
+;;(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL";
+
+;;cell config_param(int x) asm "CONFIGOPTPARAM";
+;;int cell_null?(cell c) asm "ISNULL";
+;;
+;;() raw_reserve(int amount, int mode) impure asm "RAWRESERVE";
+;;() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX";
+;;() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG";
+;;() set_code(cell new_code) impure asm "SETCODE";
+;;
+;;int random() impure asm "RANDU256";
+;;int rand(int range) impure asm "RAND";
+;;int get_seed() impure asm "RANDSEED";
+;;int set_seed() impure asm "SETRAND";
+;;() randomize(int x) impure asm "ADDRAND";
+;;() randomize_lt() impure asm "LTIME" "ADDRAND";
+;;
+;;builder store_coins(builder b, int x) asm "STVARUINT16";
+;;(slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16";
+
+int equal_slices (slice a, slice b) asm "SDEQ";
+int builder_null?(builder b) asm "ISNULL";
+int tuple_length( tuple t ) asm "TLEN";
+forall X -> int is_null(X x) asm "ISNULL";
+forall X -> int is_int(X x) asm "<{ TRY:<{ 0 PUSHINT ADD DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_cell(X x) asm "<{ TRY:<{ CTOS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_slice(X x) asm "<{ TRY:<{ SBITS DROP -1 PUSHINT }>CATCH<{ 2DROP 0 PUSHINT }> }>CONT 1 1 CALLXARGS";
+forall X -> int is_tuple(X x) asm "ISTUPLE";
+;;builder store_builder(builder to, builder from) asm "STBR";
+
diff --git a/src/toncli/projects/nft_item/func/utils/op-codes.func b/src/toncli/projects/nft_item/func/utils/op-codes.func
new file mode 100644
index 0000000..b3f8be8
--- /dev/null
+++ b/src/toncli/projects/nft_item/func/utils/op-codes.func
@@ -0,0 +1,12 @@
+const int op_transfer = 0x5fcc3d14;
+const int op_ownership_assigned = 0x05138d91;
+const int op_excesses = 0xd53276db;
+const int op_get_static_data = 0x2fcb26a2;
+const int op_report_static_data = 0x8b771735;
+const int op_get_royalty_params = 0x693d3950;
+const int op_report_royalty_params = 0xa8cb00ad;
+
+;; NFTEditable
+const int op_edit_content = 0x1a0b9d51;
+const int op_transfer_editorship = 0x1c04412a;
+const int op_editorship_assigned = 0x511a4463;
diff --git a/src/toncli/projects/nft_item/func/utils/params.func b/src/toncli/projects/nft_item/func/utils/params.func
new file mode 100644
index 0000000..eaaaa27
--- /dev/null
+++ b/src/toncli/projects/nft_item/func/utils/params.func
@@ -0,0 +1,6 @@
+const int workchain = 0;
+
+() force_chain(slice addr) impure {
+  (int wc, _) = parse_std_addr(addr);
+  throw_unless(333, wc == workchain);
+}
diff --git a/src/toncli/projects/nft_item/project.yaml b/src/toncli/projects/nft_item/project.yaml
new file mode 100644
index 0000000..495de2e
--- /dev/null
+++ b/src/toncli/projects/nft_item/project.yaml
@@ -0,0 +1,10 @@
+nft_item:
+  data: fift/nft-data.fif
+  func:
+    - func/utils/op-codes.func
+    - func/utils/params.func
+    - func/utils/helpers.func
+    - func/nft-item.func
+  tests:
+    - tests/nft-tests.func
+    - tests/nft-tests-int.func


Test patch:
diff --git a/docs/advanced/func_tests_deprecated.md b/docs/advanced/func_tests_deprecated.md
deleted file mode 100644
index 46bd6cf..0000000
--- a/docs/advanced/func_tests_deprecated.md
+++ /dev/null
@@ -1,33 +0,0 @@
-## How func tests works?
-
-Example of using func tests could be founded [here](https://github.com/disintar/func-tests-playground/blob/master/tests/example.func).
-
-But how it works and how to run it without toncli if needed?
-
-To compile test func code we use [this](https://github.com/disintar/toncli/blob/master/src/toncli/lib/func-libs/tests-helpers.func) mini library.
-
-To run `run_tests.fif` we use special `AsmTests.fif` from [here](https://github.com/disintar/toncli/blob/master/src/toncli/lib/fift-libs/AsmTests.fif)
-
-Let's start with a point where contract code and tests code successfully compiled into Fift ASM. 
-
-Toncli gets paths to those files and paste them to `run_tests.fif` [here](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L4) and [here](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L8)
-
-Also, it parse verbose level and pass it [here](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L12)
-
-`run_test.fif` parse function declaration from compiled Fift ASM and invoke running VM 3 times. 
-
-- First time [run](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L95): run [get_data](https://github.com/disintar/func-tests-playground/blob/a9810ff4c1c639fb4e9cc6541de83c6ad351b921/tests/example.func#L13) function and collect `gas-limit` / `c7` / `c4` / `stack` / `function selector`
-- Second time [run](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L127): run contract `code` with all needed params from step (1)
-- Third time [run](https://github.com/disintar/toncli/blob/5accd1562296b25c73efcae410c76905d18176be/src/toncli/modules/fift/run_test.fif.template#L142): get `c4` / `c5` / `gas-used` / `stack` / `exit-code` params from step (2) and pass them to actual [test](https://github.com/disintar/func-tests-playground/blob/a9810ff4c1c639fb4e9cc6541de83c6ad351b921/tests/example.func#L52) 
-
-### Hints
-
-1. If you want to create working functions in tests code, please use `inline method_id` 
-2. If you want to get `c4` / `c5` from previous tests - use `get_prev_c4` / `get_prev_c5`
-3. You may use `dump_stack();` for debugging
-4. You may use `-v 1` / `-v 2`  for debugging when running tests
-   1. If `-v 2` is used - stack will be dumped, but it's buggy function, so do not panic if something go wrong, just try to use `-v 1`
-   2. `-v 3` / `-v 4` used for debugging tests functions (tvm op / stack dump)
-5. You can use special `get_prev_c4` / `get_prev_c5` functions to get `c4` / `c5` cells from prev tests
-6. Last gas parameter can be tuple or int. If int - just set gas limit. If tuple of 2 ints - will set gas limit and hard gas limit
-   1. This can help when you debug `accept_message()` for example 
\ No newline at end of file
diff --git a/docs/advanced/func_tests_new.md b/docs/advanced/func_tests_new.md
index 93805c2..1caa153 100644
--- a/docs/advanced/func_tests_new.md
+++ b/docs/advanced/func_tests_new.md
@@ -1,11 +1,10 @@
 # New version of the test framework!
 
 New tests use new tvm op codes, which are not available in the stable version of TON, so you need to install the dev version of func/fift. 
-You need to compile func/fift binaries from **toncli-local** branch of ton from [here](https://github.com/SpyCheese/ton/tree/toncli-local). 
+You need to compile func/fift binaries from **toncli-local** branch of ton from [here](https://github.com/SpyCheese/ton/tree/toncli-local). (see [INSTALLATION.md](https://github.com/disintar/toncli/blob/master/INSTALLATION.md))
 You also need to make sure you use new versions of [Asm.fif](/src/toncli/lib/fift-libs/Asm.fif) and 
-[AsmTests.fif](/src/toncli/lib/fift-libs/AsmTests.fif) from this repository. 
+[AsmTests.fif](/src/toncli/lib/fift-libs/AsmTests.fif) from this repository.
 
-**TODO: write install instructions?**
 
 **Note:** if you try to run tests written in an old way with a new toncli version, you will probably see an error 
 like `PROC:<{:procedure already defined`. You need to either rewrite tests in a new way (which is preferred!) or pass `--old` flags as argument of `toncli run_tests`.  
@@ -27,7 +26,7 @@ They catch exceptions and compute gas usage.
 
 ## Examples
 
-A lot of tests written in a new way could be found [here](https://github.com/BorysMinaiev/func-contest-1-tests-playground).
+A lot of tests written in a new way could be found in `nft_collection` and `nft_item` projects examples. Also, a lot of helpers in `tests-libs` can be founded.
 
 Assume we have a contract, which stores the 64-bit integer inside, increases it when receiving an internal message, and reports current value on `get_total`.
 We can write a test like this:
diff --git a/src/toncli/lib/test-libs/message_helpers.func b/src/toncli/lib/test-libs/message_helpers.func
index c1bb743..32df764 100644
--- a/src/toncli/lib/test-libs/message_helpers.func
+++ b/src/toncli/lib/test-libs/message_helpers.func
@@ -1,24 +1,24 @@
 builder generate_init_state() impure method_id {
     return begin_cell().store_uint(0, 1) ;; maybe (##5)
-                      .store_uint(1, 1) ;; Maybe TickTock
-                      .store_uint(1, 1) ;; bool Tick
-                      .store_uint(0, 1) ;; bool Tock
-                      .store_uint(0, 1) ;; code: Maybe Cell^
-                      .store_uint(0, 1) ;; data: Maybe Cell^
-                      .store_uint(0, 1); ;; library: Maybe ^Cell
+            .store_uint(1, 1) ;; Maybe TickTock
+            .store_uint(1, 1) ;; bool Tick
+            .store_uint(0, 1) ;; bool Tock
+            .store_uint(0, 1) ;; code: Maybe Cell^
+            .store_uint(0, 1) ;; data: Maybe Cell^
+            .store_uint(0, 1); ;; library: Maybe ^Cell
 }
 
 builder generate_init_state_with_data(cell code, cell data, cell library) impure method_id {
     return begin_cell().store_uint(0, 1) ;; maybe (##5)
-                      .store_uint(1, 1) ;; Maybe TickTock
-                      .store_uint(1, 1) ;; bool Tick
-                      .store_uint(0, 1) ;; bool Tock
-                      .store_uint(1, 1) ;; code: Maybe Cell^
-                      .store_ref(code)  
-                      .store_uint(1, 1) ;; data: Maybe Cell^
-                      .store_ref(data)  
-                      .store_uint(1, 1) ;; library: Maybe ^Cell
-                      .store_ref(library);
+            .store_uint(1, 1) ;; Maybe TickTock
+            .store_uint(1, 1) ;; bool Tick
+            .store_uint(0, 1) ;; bool Tock
+            .store_uint(1, 1) ;; code: Maybe Cell^
+            .store_ref(code)
+            .store_uint(1, 1) ;; data: Maybe Cell^
+            .store_ref(data)
+            .store_uint(1, 1) ;; library: Maybe ^Cell
+            .store_ref(library);
 }
 
 tuple parse_init_state(slice cs) impure method_id {
@@ -26,36 +26,262 @@ tuple parse_init_state(slice cs) impure method_id {
     int maybe_five = cs~load_uint(1);
     parsed_tuple~tpush(maybe_five);
     if(maybe_five == 1) {
-      parsed_tuple~tpush(cs~load_uint(5)); ;; (##5)
+        parsed_tuple~tpush(cs~load_uint(5)); ;; (##5)
     }
     int maybe_tick_tock = cs~load_uint(1);
     parsed_tuple~tpush(maybe_tick_tock);
     if(maybe_tick_tock == 1) {
-      parsed_tuple~tpush(cs~load_uint(1)); ;; tick
-      parsed_tuple~tpush(cs~load_uint(1)); ;; tock
+        parsed_tuple~tpush(cs~load_uint(1)); ;; tick
+        parsed_tuple~tpush(cs~load_uint(1)); ;; tock
     }
     int maybe_code = cs~load_uint(1);
     parsed_tuple~tpush(maybe_code);
     if(maybe_code == 1) {
-      parsed_tuple~tpush(cs~load_ref()); ;; code
+        parsed_tuple~tpush(cs~load_ref()); ;; code
     }
-    
+
     int maybe_data = cs~load_uint(1);
     parsed_tuple~tpush(maybe_data);
     if(maybe_data == 1) {
-      parsed_tuple~tpush(cs~load_ref()); ;; data
+        parsed_tuple~tpush(cs~load_ref()); ;; data
     }
 
     int maybe_library = cs~load_uint(1);
     parsed_tuple~tpush(maybe_library);
     if(maybe_library == 1) {
-      parsed_tuple~tpush(cs~load_ref()); ;; library
+        parsed_tuple~tpush(cs~load_ref()); ;; library
     }
 
     ;; returns tuple with results that exist only
     return parsed_tuple;
 }
 
+int random_query_id() impure method_id {
+    randomize_lt();
+    return rand(1 << 64) + 1; ;; From 1 to 2^64
+}
+
+builder generate_internal_message_body(int op, int query_id) impure method_id {
+    builder body = begin_cell().store_uint(op, 32);
+
+    if (query_id == 0) {
+        query_id = random_query_id();
+    }
+
+    return body.store_uint(query_id, 64);
+}
+
+
+cell generate_internal_message_custom(int bounce, int ton_amount, int init_state, builder payload, slice  src_addr, slice dst_addr, int fwd_fee) impure method_id {
+    ;;int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
+    ;;src:MsgAddressInt dest:MsgAddressInt
+    ;;value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
+    ;;created_lt:uint64 created_at:uint32 = CommonMsgInfo;
+
+    ;;message$_ {X:Type} info:CommonMsgInfo
+    ;;init:(Maybe (Either StateInit ^StateInit))
+    ;;body:(Either X ^X) = Message X;
+
+    slice ssrc = null?(src_addr) ? generate_internal_address() : src_addr;
+    slice sdest = null?(dst_addr) ? generate_internal_address() : dst_addr;
+
+    var msg = begin_cell()
+            .store_uint(0, 1) ;; int_msg_info$0 
+            .store_uint(0, 1) ;; ihr_disabled:Bool
+            .store_uint(bounce, 1) ;; bounce:Bool
+            .store_uint(0, 1) ;; bounced:Bool
+            .store_slice(ssrc) ;; src:MsgAddressInt
+            .store_slice(sdest) ;; dest:MsgAddressInt
+            .store_grams(ton_amount) ;;
+            .store_dict(null()) ;; extra currencies
+            .store_grams(0) ;; ihr_fee
+            .store_grams(fwd_fee) ;; fwd_fee
+            .store_uint(1000, 64) ;; created_lt:uint64
+            .store_uint(1000, 32) ;; created_at:uint32
+            .store_uint(init_state, 1); ;; init_state
+    if (init_state == 1) {
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
+    }
+
+    if(msg.builder_bits() + payload.builder_bits() >= 1023) {
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
+    } else {
+        msg = msg.store_uint(0, 1).store_builder(payload);
+    }
+
+    return msg.end_cell();
+
+}
+
+builder generate_get_royalty_params( int  query_id ) {
+	;;get_royalty_params#693d3950 query_id:uint64 = InternalMsgBody;
+	return generate_internal_message_body( 0x693d3950, query_id );
+}
+
+builder generate_nft_transfer_request(slice new_owner, slice response_dst, int query_id, cell custom_payload, int forward_amount, cell forward_payload, int is_ref?) impure method_id {
+    ;;transfer#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress
+    ;; custom_payload:(Maybe ^Cell)
+    ;;forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+    ;;= InternalMsgBody;
+
+    builder req = generate_internal_message_body(0x5fcc3d14, query_id)
+            .store_slice(new_owner)
+            .store_slice(response_dst);
+
+    if (null?(custom_payload)) {
+		req = req.store_uint(0, 1);
+    }
+    else {
+		req = req.store_uint(1, 1).store_ref(custom_payload);
+    }
+
+    req = req.store_grams(forward_amount);
+
+    if (~ null?(forward_payload)) {
+        if (is_ref?) {
+			req = req.store_uint(1, 1).store_ref(forward_payload);
+        }
+        else {
+			req = req.store_uint(0, 1).store_slice(forward_payload.begin_parse());
+        }
+    }
+    else {
+		req = req.store_uint(0, 1);
+    }
+
+    return req;
+}
+
+
+builder generate_nft_get_static_data_request(int query_id) impure method_id {
+    ;;get_static_data#2fcb26a2 query_id:uint64 = InternalMsgBody;
+    return begin_cell().store_uint(0x2fcb26a2, 32).store_uint(query_id, 64);
+}
+
+builder generate_nft_deploy_request(int idx, cell content, int query_id, int forward_amount) impure method_id {
+    return generate_internal_message_body(1, query_id)
+            .store_uint(idx, 64)
+            .store_grams(forward_amount)
+            .store_ref(content);
+}
+
+builder generate_jetton_burn_request(int query_id, amount, slice dst, cell custom_payload) impure method_id {
+    {-
+	burn#595f07bc query_id:uint64 amount:(VarUInteger 16) 
+              response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+              = InternalMsgBody;
+
+	-}
+
+    builder burn_msg = generate_internal_message_body(0x595f07bc, query_id)
+            .store_grams(amount)
+            .store_slice(dst);
+    if(~ null?(custom_payload)) {
+        burn_msg = burn_msg.store_uint(1, 1).store_ref(custom_payload);
+    }
+    else {
+		burn_msg = burn_msg.store_uint(0, 1);
+    }
+
+	return burn_msg;
+}
+
+builder generate_jetton_burn_notification(int query_id, int amount, slice sender, slice resp_dst) {
+    return generate_internal_message_body(0x7bdd97de, query_id).store_grams(amount)
+            .store_slice(sender)
+            .store_slice(resp_dst);
+}
+
+builder generate_jetton_internal_transfer_request(
+        int query_id, int amount,
+		slice from, slice resp_addr,
+		int forward_amount, cell forward_payload,
+		int is_ref? ) impure method_id {
+	{-
+
+	  Unspecified by standard but suggested format of internal message
+
+	  internal_transfer  query_id:uint64 amount:(VarUInteger 16) from:MsgAddress
+                     response_address:MsgAddress
+                     forward_ton_amount:(VarUInteger 16)
+                     forward_payload:(Either Cell ^Cell)
+                     = InternalMsgBody;
+	-}
+
+	builder req = generate_internal_message_body( 0x178d4519, query_id )
+	               .store_grams( amount )
+				   .store_slice( from )
+				   .store_slice( resp_addr )
+				   .store_grams( forward_amount );
+
+	if( ~ forward_payload.null?() ){
+		if( is_ref? ) {
+			req = req.store_uint( 1, 1 ).store_ref( forward_payload );
+		}
+		else {
+			req = req.store_uint( 0, 1 ).store_slice( forward_payload.begin_parse() );
+		}
+	}
+	else {
+		req = req.store_uint( 0, 1 );
+	}
+
+	return req;
+
+}
+
+builder generate_jetton_transfer_request(
+        int query_id, int amount,
+        slice dst, slice resp_dst,
+        cell custom_payload, int forward_amount,
+        cell forward_payload, int is_ref?) impure method_id {
+    {-
+
+	transfer#0f8a7ea5 query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
+                 response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+                 forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+                 = InternalMsgBody;
+
+	-}
+
+    var req = generate_internal_message_body(0x0f8a7ea5, query_id)
+            .store_grams(amount)
+            .store_slice(dst)
+            .store_slice(resp_dst);
+
+    if(null?(custom_payload)) {
+        req = req.store_uint(0, 1);
+    }
+    else {
+        req = req.store_uint(1, 1).store_ref(custom_payload);
+    }
+
+    req = req.store_grams(forward_amount);
+
+    if(~ null?(forward_payload)) {
+       if(is_ref?) {
+       	req = req.store_uint(1, 1).store_ref(forward_payload);
+       }
+       else {
+       	req = req.store_uint(0, 1).store_slice(forward_payload.begin_parse());
+       }
+    }
+    else {
+    	req = req.store_uint(0, 1);
+    }
+
+    return req;
+}
+
+
 cell generate_internal_message(int bounce, int ton_amount, int init_state, builder payload) impure method_id {
     ;;int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
     ;;src:MsgAddressInt dest:MsgAddressInt
@@ -84,21 +310,21 @@ cell generate_internal_message(int bounce, int ton_amount, int init_state, build
             .store_uint(1000, 32) ;; created_at:uint32
             .store_uint(init_state, 1); ;; init_state
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
     }
-    
+
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -137,21 +363,21 @@ cell generate_internal_message_relaxed(int bounce, int ton_amount, int init_stat
             .store_uint(1000, 32) ;; created_at:uint32
             .store_uint(init_state, 1); ;; init_state
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
-    }
-    
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
+    }
+
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -175,21 +401,21 @@ cell generate_external_in_message(int import_fee, int init_state, builder payloa
             .store_grams(import_fee) ;;
             .store_uint(init_state, 1); ;; init_state
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
-    }
-    
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
+    }
+
     if(msg.builder_bits() + payload.builder_bits() > 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -214,21 +440,21 @@ cell generate_external_in_message_with_empty_source_address(int import_fee, int
             .store_uint(init_state, 1); ;; init_state
 
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
     }
 
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -252,23 +478,23 @@ cell generate_external_out_message(int import_fee, int init_state, builder paylo
             .store_uint(1000, 64) ;; created_lt:uint64
             .store_uint(1000, 32) ;; created_at:uint32
             .store_uint(init_state, 1); ;; init_state
-    
+
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
-    }
-    
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
+    }
+
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -293,21 +519,21 @@ cell generate_external_out_message_with_empty_destination_address(int import_fee
             .store_uint(1000, 32) ;; created_at:uint32
             .store_uint(init_state, 1); ;; init_state
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
     }
 
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -337,21 +563,21 @@ cell generate_external_out_message_relaxed(int ton_amount, int init_state, build
             .store_uint(1000, 32) ;; created_at:uint32
             .store_uint(init_state, 1); ;; init_state
     if (init_state == 1) {
-      builder init_state_builder = generate_init_state();
-      
-      if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
-        msg = msg.store_uint(1,1) ;; Either ^StateInit
-            .store_ref(init_state_builder.end_cell());
-      } else {
-        msg = msg.store_uint(0,1).store_builder(init_state_builder);
-      }
-    }
-    
+        builder init_state_builder = generate_init_state();
+
+        if(msg.builder_bits() + init_state_builder.builder_bits() >= 1023) {
+            msg = msg.store_uint(1, 1) ;; Either ^StateInit
+                    .store_ref(init_state_builder.end_cell());
+        } else {
+            msg = msg.store_uint(0, 1).store_builder(init_state_builder);
+        }
+    }
+
     if(msg.builder_bits() + payload.builder_bits() >= 1023) {
-      msg = msg.store_uint(1,1) ;; Either ^X
-            .store_ref(payload.end_cell());
+        msg = msg.store_uint(1, 1) ;; Either ^X
+                .store_ref(payload.end_cell());
     } else {
-      msg = msg.store_uint(0,1).store_builder(payload);
+        msg = msg.store_uint(0, 1).store_builder(payload);
     }
 
     return msg.end_cell();
@@ -375,16 +601,16 @@ cell generate_external_out_message_relaxed(int ton_amount, int init_state, build
     int maybe_init_state = cs~load_uint(1);
     tuple init_state = empty_tuple();
     if(maybe_init_state == 1)
-    {
-      ;;Either bit was ignored before
-
-      if( cs~load_uint(1) ) {
-      	init_state = parse_init_state( cs~load_ref().begin_parse() );
-      }
-      else {
-      	init_state = parse_init_state(cs);
-      }
-    }
+            {
+                ;;Either bit was ignored before
+
+                if(cs~load_uint(1)) {
+                    init_state = parse_init_state(cs~load_ref().begin_parse());
+                }
+                        else {
+                            init_state = parse_init_state(cs);
+                        }
+            }
     int body_flag = cs~load_uint(1);
     var body = null();
     if (body_flag == 0) {
@@ -408,20 +634,19 @@ cell generate_external_out_message_relaxed(int ton_amount, int init_state, build
     slice to_address = cs~load_msg_addr();
     int import_fee = 0;
     int timestamps = 0;
-    if (msg_info == 2)
-    {
-      import_fee = cs~load_grams();
-    }
-    else
-    {
-      timestamps = cs~load_uint(96);
+    if (msg_info == 2) {
+        import_fee = cs~load_grams();
+    } else {
+        timestamps = cs~load_uint(96);
     }
+
     int maybe_init_state = cs~load_uint(1);
     tuple init_state = empty_tuple();
-    if(maybe_init_state == 1)
-    {
-      init_state = parse_init_state(cs);
+
+    if (maybe_init_state == 1)  {
+        init_state = parse_init_state(cs);
     }
+
     int body_flag = cs~load_uint(1);
 
     var body = null();
@@ -434,4 +659,4 @@ cell generate_external_out_message_relaxed(int ton_amount, int init_state, build
     }
 
     return [src, to_address, import_fee, timestamps, init_state, body];
-}
\ No newline at end of file
+}
diff --git a/src/toncli/lib/test-libs/token-helpers.func b/src/toncli/lib/test-libs/token-helpers.func
new file mode 100644
index 0000000..0c70ad7
--- /dev/null
+++ b/src/toncli/lib/test-libs/token-helpers.func
@@ -0,0 +1,105 @@
+int slcmp(slice a, slice b) asm "SDEQ";
+
+int token_snake_len( slice content) impure inline {
+	int len = 0;
+
+	while( ~ null?( content ) ){
+		len       += content.slice_bits();
+		content    = content.slice_refs() ? content~load_ref().begin_parse() : null();
+	} 
+
+	return len;
+
+}
+
+cell get_snake_tail( cell tail ) impure inline {
+
+	while( ~ tail.null?() ) {
+		slice tail_slice = tail.begin_parse();
+		tail	   	 = tail_slice.slice_refs() ? tail_slice~load_ref() : null();
+	}
+
+	return tail;
+}
+
+cell snake_concat( cell head, cell tail ) impure method_id {
+	slice	head_sl  = head.begin_parse();
+	;; store_slice, as i've discovered, copies slice with refs ( if any )
+	;; We don't need refs since those are copied manually down below
+	builder snake 	 = begin_cell().store_slice( head_sl~load_bits( head_sl.slice_bits() ) );
+
+	if( head_sl.slice_refs() ) {
+		snake = snake.store_ref( snake_concat( head_sl~load_ref(), tail ) );
+	}
+	else { 
+		snake = snake.store_ref( tail );
+	}
+
+	return snake.end_cell();
+}
+
+cell snake_concat_tagged( int tag, cell head, cell tail ) impure inline {
+	slice   head_sl  = head.begin_parse();
+	builder snake	 = begin_cell().store_uint( tag, 8 );
+	cell	tagged   = null();
+	int 	lbits	 = 1023 - 8; ;;Bits left after prepending tag
+
+	if( head_sl.slice_bits() > lbits ) {
+		;; If we can't fit it we just ref the concat result
+		tagged = snake.store_ref( snake_concat( head, tail )).end_cell();
+	}
+	else {
+		;; Otherwise squeeze it into one and concat 
+		tagged = snake_concat( snake.store_slice( head_sl ).end_cell(), tail );
+	}
+
+	
+
+	return tagged;
+
+}
+
+int snake_equal?( slice snake1, slice snake2 ) impure inline {
+	int equal = true;
+
+	do {
+		int s1_data = snake1.slice_bits();
+		int s2_data = snake2.slice_bits();
+
+		if( s1_data <= s2_data ) {
+
+			if( s1_data > 0 ) {
+				;; Compare shortest slice
+				equal  = slcmp( snake1, snake2~load_bits( s1_data ) );
+			}
+
+			;; Snake1 guaranteed no data left
+			snake1 = snake1.slice_refs() ? snake1~load_ref().begin_parse() : null();
+
+			;; If snake1 has no refs and s2 still has data
+			;; Those are not equal
+			if( snake1.null?() & s2_data > s1_data ) {
+				equal = false;
+			}
+			else {
+				;; Snake one still has refs
+				;; If data chunks are equal means snake2 is ended too
+				if( s2_data == s1_data ){
+					snake2 = snake2.slice_refs() ? snake2~load_ref().begin_parse() : null();
+				}
+			}
+
+		}
+		else {
+			equal  = slcmp( snake2, snake1~load_bits( s2_data ) );
+
+			snake2 = snake2.slice_refs() ? snake2~load_ref().begin_parse() : null();
+		}
+			
+	} until( ~ equal | ( snake1.null?() & snake2.null?() ) )
+
+	return equal;
+
+}
+
+
diff --git a/src/toncli/modules/fift/run_test.fif.template b/src/toncli/modules/fift/run_test.fif.template
index 1be2f7d..0c5cce2 100644
--- a/src/toncli/modules/fift/run_test.fif.template
+++ b/src/toncli/modules/fift/run_test.fif.template
@@ -2,18 +2,35 @@
 "TonUtil.fif" include
 "Color.fif" include
 
+// TODO: just use Lists.fif
+variable @tests            // here we will store all test functions in a format [function_name, method_id]
+variable @prev_c4            // prev test c4
+variable @prev_c5            // prev test c5
+variable @init_c4 // Init value of c4
+null @prev_c4 !
+null @prev_c5 !
+
+{   depth 0 >
+    { 0 = not }
+    { drop depth }
+    while // clear stack
+} : clear-stack
+
+{% if contract_data|length %}
+  "{{ contract_data }}" include @init_c4 ! clear-stack
+  .s
+{% else %}
+  <b b>  @init_c4 !
+{% endif %}
+
 asm-no-remove-unused
 
 "{{ test_path }}" include     // it will return (@proclist, code cell)
 constant test_code         // save code cell as constant
 
-
 {{ verbose }} constant verbose
 {{ output_results }} constant output-results
 
-{% if contract_data|length %}
-"{{ contract_data }}" constant proj_data 
-{% endif %}
 
 "EQB36_EfYjFYMV8p_cxSYX61bA0FZ4B65ZNN6L8INY-5gL6w" true parse-load-address drop 2constant address
 
@@ -31,22 +48,6 @@ null                 // global_config
 10 tuple 1 tuple constant c7 // save as c7 cell
 
 
-// TODO: just use Lists.fif
-variable @tests            // here we will store all test functions in a format [function_name, method_id]
-variable @prev_c4            // prev test c4
-variable @prev_c5            // prev test c5
-variable @init_c4 // Init value of c4
-null @prev_c4 !
-null @prev_c5 !
-
-// We usually (Eh) don't have procedures defined in data.fif so include returns just cell
-// What if someone adds method to it? No good. What can we do? 
-// IDK since we don't have a way to get stack size. Or do we?
-// Maybe move it to the very top before asm-mode changes ( L5 )?
-
-def? proj_data { @' proj_data include } { <b b>  } cond
-@init_c4 ! 
-
 variable @cnt        // here we will store tests list length
 @cnt 0!              // set as 0 by default
 
diff --git a/src/toncli/modules/utils/test/__init__.py b/src/toncli/modules/utils/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/tests/.gitkeep b/src/toncli/projects/jetton_minter/tests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_minter/tests/minter-tests-int.func b/src/toncli/projects/jetton_minter/tests/minter-tests-int.func
new file mode 100644
index 0000000..e957726
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/tests/minter-tests-int.func
@@ -0,0 +1,96 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+
+int __test_get_jetton_data_mint() {
+
+	var( gas_before, stack ) = invoke_method( get_jetton_data, [] );
+
+	int total_supply = stack.first();
+	int mintable?    = stack.second();
+	slice admin      = stack.third();
+	cell content     = stack.fourth();
+	cell code        = stack.at( 4 );
+
+
+	;; Can't test non mintable contract with mint message.
+	if( ~ mintable? ) {
+		return gas_before;
+	}
+
+	int query_id     = rand(1337) + 1;
+	int forward_ton  = one_unit / 10;
+	int mint_amount  = ( rand( 100 ) + 1 ) * one_unit / 10;
+	slice rand_addr  = generate_internal_address_with_custom_data(0, 0, random());
+	slice mint_dst   = generate_internal_address_with_custom_data(0, 0, random());
+	slice mint_from  = generate_empty_address();
+	cell mint_payload = generate_jetton_internal_transfer_request( query_id, mint_amount, mint_from, rand_addr, forward_ton, null(), false ).end_cell();
+	builder mint_body = generate_internal_message_body( op_mint, query_id ).store_slice( mint_dst ).store_grams( forward_ton ).store_ref( mint_payload );
+	;; Testing mint with non-admin address
+	cell msg          = generate_internal_message_custom( 0, 0, 0,  mint_body, admin, null(), 0 );
+
+	var( gas_mint, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] );
+	var( gas_after, stack ) = invoke_method( get_jetton_data, [] );
+	;; Total supply should increase by our mint_amount
+	throw_unless( 500, total_supply + mint_amount == stack.first() );
+
+	;; Rest should not change
+
+	throw_unless( 501, mintable? == stack.second() );
+	throw_unless( 502, equal_slices( admin, stack.third() ) );
+	throw_unless( 503, equal_slices( content.begin_parse(), stack.fourth().begin_parse() ) );
+	throw_unless( 504, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) );
+
+
+	return gas_before + gas_mint + gas_after;
+}
+
+int __test_get_jetton_data_burn() {
+
+	var( gas_before, stack ) = invoke_method( get_jetton_data, [] );
+
+	int total_supply = stack.first();
+	int mintable?    = stack.second();
+	slice admin      = stack.third();
+	cell content     = stack.fourth();
+	cell  code       = stack.at( 4 );
+
+	;;Nothing to burn
+	if( total_supply == 0 ) {
+		return gas_before;
+	}
+
+	{-
+	 If jetton is not mintabe, does it mean that it's necesserely
+	 non-burnable too? IDK.
+	 I'd say doesn't and burnable? flag required.
+	 So we would ignore mintable? in this test.
+	-}
+
+	int query_id     = rand(1337) + 1;
+	int forward_ton  = one_unit / 10;
+	int burn_amount  = ( rand( 100 ) + 1 ) * one_unit / 10;
+
+	slice jetton_addr = my_address(); ;;In testing mode my_address() for test code equals  my_address() for contract code
+	slice sender      = generate_internal_address_with_custom_data( 0, 0, random() );
+	slice src	      = calculate_user_jetton_wallet_address( sender, jetton_addr, code );
+	var msg_body      = generate_jetton_burn_notification( query_id, burn_amount, sender, admin );
+	cell msg          = generate_internal_message_custom( 0, 0, 0, msg_body, src, null(), 0);
+	
+	( int gas_burn, _ ) = invoke_method( recv_internal, [one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	( int gas_after, stack ) = invoke_method( get_jetton_data, [] );
+
+
+	throw_unless( 500, total_supply - burn_amount == stack.first() );
+
+	;; Rest should not change
+
+	throw_unless( 501, mintable? == stack.second() );
+	throw_unless( 502, equal_slices( admin, stack.third() ) );
+	throw_unless( 503, equal_slices( content.begin_parse(), stack.fourth().begin_parse() ) );
+	throw_unless( 504, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) );
+
+	return gas_before + gas_burn + gas_after;
+}
diff --git a/src/toncli/projects/jetton_minter/tests/minter-tests.func b/src/toncli/projects/jetton_minter/tests/minter-tests.func
new file mode 100644
index 0000000..0837718
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/tests/minter-tests.func
@@ -0,0 +1,203 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+(int, slice, cell, cell) load_test_data() inline {
+  slice ds = get_data().begin_parse();
+  return (
+      ds~load_coins(), ;; total_supply
+      ds~load_msg_addr(), ;; admin_address
+      ds~load_ref(), ;; content
+      ds~load_ref()  ;; jetton_wallet_code
+  );
+}
+
+_ verify_excess_jetton( int query_id, slice resp_dst, int msg_value, int forward_fee, int forward_amount, cell msg ) impure inline {
+	{-
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+	Excess message should be sent to resp_dst with all of the msg_value - fees taken to process
+	We verify that:
+	1) message is sent to resp_dst
+	2) attached amount is at least msg_value - forward_fee * 2
+	3) op matches excess op
+	4) query_id matches request query_id
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;;Check dst_addr to be equal to resp_dst
+	throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) );
+
+	int total_sent  = parsed_msg.at( 5 );
+	int should_sent = msg_value - forward_amount - forward_fee * 2;
+
+	throw_unless( 702, total_sent >= should_sent );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) );
+
+	throw_unless( 704, query_id == msg_body~load_uint( 64 ) );
+}
+
+_ validate_TIP_64( slice content_data ) impure inline {
+
+	int content_layout = content_data~load_uint( 8 );
+
+	;; Check for allowed content_layout
+	throw_unless( 305, ( content_layout  == 1 ) | ( content_layout == 0 ) );
+
+	if( content_layout == 1 ){
+
+		;; Check that off-chain URI contains at least one ASCII char
+		throw_unless( 306, token_snake_len( content_data ) > 8 );
+	} else {
+		;; On-chain is stored as dict
+		;; Has to be non-empty
+		throw_unless( 306, content_data.preload_dict().dict_empty?() );
+
+		;; Perhaps could go further and test for Optional dict keys but none of those are required so i'll leave it be
+		;; For now
+	}
+}
+
+int __test_burn_notification() {
+	{-
+	  burn_notification query_id:uint64 amount:(VarUInteger 16)
+	       sender:MsgAddress response_destination:MsgAddress
+	       = InternalMsgBody;
+
+	  On receiving burn notification jetton master should
+	  1) Decrease total supply by burn_amount.
+	  2) Send excess message to the response_destination.
+	  
+	-}
+	var ( total_supply, admin_address, content, code ) = load_test_data(); 
+
+	int query_id = rand( 1337 ) + 1;
+	int burn_amount = total_supply / 10;
+	;; Making sure that source address would be valid jetton address
+
+	slice sender    = generate_internal_address_with_custom_data( 0, 0, random() );
+	slice src	= calculate_user_jetton_wallet_address( sender, my_address(), code );
+	var msg_body    = generate_jetton_burn_notification( query_id, burn_amount, sender, admin_address );
+	cell msg        = generate_internal_message_custom( 0, 0, 0, msg_body, src, null(), 0);
+
+	( int gas_used, _ ) = invoke_method( recv_internal, [one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Expect single message
+	tuple actions	    = parse_c5();
+	throw_unless( 600, actions.tuple_length() == 1 );
+
+	( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple();
+	throw_unless( 601, action_type == 0 );
+	throw_unless( 602, ( mode >= 64 ) & ( mode <= 66 ) );
+
+	;; it's mode 64 so msg_value - fees return guaranteed
+	;; Still have to verify excess structure
+
+	verify_excess_jetton( query_id, admin_address, 0, 0, 0, sent_msg );
+
+	var( new_supply, _, _, _ ) = load_test_data(); 
+
+	throw_unless( 603, total_supply - burn_amount == new_supply );
+	
+
+	return gas_used;
+}
+
+int __test_mint() {
+
+	;;Non-standardized but widely used message
+	var ( total_supply, admin_address, content, code ) = load_test_data();
+
+	int query_id     = rand(1337) + 1;
+	int forward_ton  = one_unit / 10;
+	int mint_amount  = ( rand( 100 ) + 1 ) * one_unit / 10;
+	slice rand_addr  = generate_internal_address_with_custom_data(0, 0, random());
+	slice mint_dst   = generate_internal_address_with_custom_data(0, 0, random());
+	slice mint_from  = generate_empty_address();
+
+	{-
+		In current implementation sends any forward payload provided.
+		However for jettons to be successfully creditated on wallet balance
+		it should have internal_transfer message format.
+	-}
+
+	cell mint_payload = generate_jetton_internal_transfer_request( query_id, mint_amount, mint_from, rand_addr, forward_ton, null(), false ).end_cell();
+	builder mint_body = generate_internal_message_body( op_mint, query_id ).store_slice( mint_dst ).store_grams( forward_ton ).store_ref( mint_payload );
+	;; Testing mint with non-admin address
+	cell msg          = generate_internal_message_custom( 0, 0, 0,  mint_body, rand_addr, null(), 0 );
+
+	int gas_failed    = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;;Now changing source address to admin
+	msg               = generate_internal_message_custom( 0, 0, 0,  mint_body, admin_address, null(), 0 );
+
+	var( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, mint_body.end_cell().begin_parse() ] );
+
+	;; Expect single message
+	tuple actions	    = parse_c5();
+	throw_unless( 600, actions.tuple_length() == 1 );
+
+	( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple();
+	throw_unless( 601, action_type == 0 );
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( sent_msg ) );
+
+	{-
+	  What do we know about how mint message should look like in general?
+	  1) Wallet has to be deployed thus StateInit has to be present
+	  2) Our forward payload should be the message body
+	  That's what we're going to check for.
+	  Rest is very contract specific.
+	-}
+
+	throw_if( 602, parsed_msg.at( 7 ).null?() );
+	throw_unless( 603, equal_slices( mint_payload.begin_parse(), parsed_msg.at( 8 ) ) );
+
+	;; Also total supply should increase by mint_amount
+
+	var( new_supply, _, _, _ ) = load_test_data();
+	throw_unless( 604, total_supply + mint_amount == new_supply );
+
+	return gas_failed + gas_success;
+
+}
+
+;; Get methods tests start here
+
+int __test_get_jetton_data() {
+
+	int expect_mintable? = true; ;;Determines if test expects mintable contract
+
+	var ( total_supply, admin_address, content, code ) = load_test_data(); 
+
+	var ( gas_used, stack ) = invoke_method( get_jetton_data, [] ); 
+
+	throw_unless( 500, stack.tuple_length() == 5 );
+
+	throw_unless( 501, total_supply == stack.first() );
+	throw_unless( 502, expect_mintable? & stack.second() );
+	throw_unless( 503, equal_slices( admin_address, stack.third() ) );
+
+	slice res_cs = stack.fourth().begin_parse(); ;; Content slice
+
+	throw_unless( 504, equal_slices( content.begin_parse(), res_cs ) );
+	validate_TIP_64( res_cs );
+	throw_unless( 505, equal_slices( code.begin_parse(), stack.at( 4 ).begin_parse() ) );
+
+	return gas_used;
+}
+
+int __test_get_wallet_address() {
+
+	var ( gas_used, stack ) = invoke_method( get_wallet_address, [ my_address() ] );
+
+	throw_unless( 700, stack.tuple_length() == 1 );
+
+	parse_std_addr( stack.first() ); ;;I guess that's what else we can check
+
+	return gas_used;
+}
diff --git a/src/toncli/projects/jetton_minter/tests/utils/constants.func b/src/toncli/projects/jetton_minter/tests/utils/constants.func
new file mode 100644
index 0000000..28221d7
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/tests/utils/constants.func
@@ -0,0 +1,7 @@
+const int one_unit = 1000000000; ;; 10^9
+
+;; These are not standardized values each Jetton can have it's own fee guidelines
+const int jetton_min_storage = 10000000; ;;0.01 TON
+const int jetton_gas_fee     = 10000000; ;;0.01 TON
+
+
diff --git a/src/toncli/projects/jetton_minter/tests/wallet-tests-int.func b/src/toncli/projects/jetton_minter/tests/wallet-tests-int.func
new file mode 100644
index 0000000..1e509c7
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/tests/wallet-tests-int.func
@@ -0,0 +1,118 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+
+int __test_get_wallet_data_transfer() {
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance  = stack.first();
+	slice owner  = stack.second();
+	slice master = stack.third();
+	cell code    = stack.fourth();
+
+	;; Nothing to send
+
+	if( balance <= 0 ) {
+		return gas_before; ;;throw( 100 ) ?
+	}
+
+	int transfer_amount      = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance
+	int forward_amount       = one_unit / 10;
+	int forward_fee          = one_unit / 100;
+
+
+	builder msg_body         = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false );
+	cell    msg	             = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	int new_balance = stack.first();
+
+	throw_unless( 500, balance - transfer_amount == stack.first() );
+
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+
+	return gas_before + gas_transfer + gas_after;
+}
+
+int __test_get_wallet_internal_transfer() {
+
+	{-
+	 When incoming transfer happens
+	 Balance should increase accordingly
+	-}
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance         = stack.first();
+	slice owner         = stack.second();
+	slice master        = stack.third();
+	cell code           = stack.fourth();
+
+
+	int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit
+	int forward_ton     = one_unit / 10;
+	int msg_value       = one_unit;
+
+	var  msg_body       = generate_jetton_internal_transfer_request( query_id, transfer_amount, master, dst, forward_ton, null(), false );
+	cell msg            = generate_internal_message_custom( 0, 0, 0,  msg_body, master, null(), 0 );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 500, balance + transfer_amount == stack.first() );
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+	return gas_before + gas_transfer + gas_after;
+}
+
+int __test_get_wallet_data_burn() {
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance  = stack.first();
+	slice owner  = stack.second();
+	slice master = stack.third();
+	cell code    = stack.fourth();
+
+
+	;; Nothing to burn
+
+	if( balance <= 0 ) {
+		return gas_before; ;;throw( 100 ) ?
+	}
+
+	int burn_amount      = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance
+	int forward_amount   = one_unit / 10;
+	int forward_fee      = one_unit / 100;
+	var  msg_body        = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() );
+	cell msg             = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_burn, _ )  = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 500, balance - burn_amount == stack.first() );
+
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+
+
+	return gas_before + gas_burn;
+}
diff --git a/src/toncli/projects/jetton_minter/tests/wallet-tests.func b/src/toncli/projects/jetton_minter/tests/wallet-tests.func
new file mode 100644
index 0000000..8c38e83
--- /dev/null
+++ b/src/toncli/projects/jetton_minter/tests/wallet-tests.func
@@ -0,0 +1,665 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+
+( int, slice, slice, cell ) load_test_data() inline {
+
+	slice ds = get_data().begin_parse();
+	return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
+}
+
+( slice ) get_owner() inline {
+
+	slice ds = get_data().begin_parse();
+	ds~load_coins();
+
+	return ds~load_msg_addr();
+}
+
+( slice ) get_master() inline {
+	slice ds = get_data().begin_parse();
+	ds~load_coins();
+	ds~load_msg_addr();
+	return ds~load_msg_addr();
+}
+
+( int )  get_test_balance() inline {
+	slice ds = get_data().begin_parse();
+
+	return ds~load_coins();
+}
+
+( cell,() ) replace_msg_source( cell msg, slice new_addr ) inline {
+	slice   old_msg = msg.begin_parse();
+	builder new_msg = begin_cell().store_slice( old_msg~load_bits( 4 ) );
+	old_msg~load_msg_addr(); ;;Skip source addr
+	new_msg = new_msg.store_slice( new_addr ).store_slice( old_msg ); ;;Store new addr and the rest of the orig msg
+
+	return ( new_msg.end_cell(),() );
+	
+}
+
+slice gen_non_owner( slice old_owner ) inline {
+	slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	;; In theory we can win a bingo and get same 256bit integer from RNG
+	while( equal_slices( new_owner, old_owner ) ) {
+		new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+	}
+
+	return new_owner;
+}
+
+( slice, slice, slice, int ) setup_req_fields() {
+	slice owner    = get_owner();
+	slice dst      = generate_internal_address_with_custom_data( 0, 0, random() );
+	slice resp_dst = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	return ( owner, dst, resp_dst, rand( 1337 ) + 1 );
+}
+
+cell get_state_init_field( tuple state_init, int idx ) inline {
+	cell res = null();
+	;; Next flag index
+	int next_idx = 0;
+
+	do {
+		int flag = state_init.at( next_idx ); 
+
+		next_idx = flag ? next_idx + 2 : next_idx + 1;
+		idx -= 1;
+
+		if( idx == 0 & flag ) {
+			res = state_init.at( next_idx - 1 );
+		}
+
+	} until( ~ res.null?() | idx <= 0 )
+
+	return res;
+}
+
+_ verify_transfer_notification( cell msg, int jetton_amount, int query_id, slice dst, slice resp_dst, slice sender, int forward_amount, cell payload  ) impure inline {
+	{-
+	transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) 
+                              sender:MsgAddress forward_payload:(Either Cell ^Cell)
+                              = InternalMsgBody;
+
+	Transfer notification message is sent when forward_amount > 0 from receiver to sender wallet
+	1) query_id should be equal to the query_id of sender request
+	2) jetton_amount should be equal to the amount of transfered jettons
+	3) sender should be equal to the sender address
+	4) forward_payload ( if any ) should be equal to the payload of sender request
+	-}
+
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	{-
+	 Why it's dst and ton resp_dst? 
+	 I don't fully get it, but by standard:
+	 if forward_amount > 0 ensure that receiver's jetton-wallet send message to destination address with forward_amount nanotons attached
+	 Probably has something to do with separating excess and forward amount value
+	-}
+	throw_unless( 801, equal_slices( dst, parsed_msg.at( 4 ) ) );
+
+	;;forward_amount tons should be attached
+	throw_unless( 802, forward_amount == parsed_msg.at( 5 )  );
+
+	slice msg_body	= parsed_msg.at( 8 );
+
+	throw_unless( 803, op_transfer_notification == msg_body~load_uint( 32 ) );
+
+	throw_unless( 804, query_id == msg_body~load_uint( 64 ) );
+
+	throw_unless( 805, jetton_amount == msg_body~load_coins() );
+
+	throw_unless( 806, equal_slices( sender, msg_body~load_msg_addr() ) );
+
+	if( ~ null?( payload ) ) {
+		if( msg_body~load_uint( 1 ) ) {
+			throw_unless( 807, equal_slices( payload.begin_parse(), msg_body~load_ref().begin_parse() ) );
+		}
+		else {
+			throw_unless( 807, equal_slices( payload.begin_parse(), msg_body ) );
+		}
+	}
+}
+
+_ verify_burn_notification( cell msg, int query_id, int burn_amount, slice sender, slice resp_dst ) impure inline {
+	{-
+	  burn_notification query_id:uint64 amount:(VarUInteger 16)
+	       sender:MsgAddress response_destination:MsgAddress
+	       = InternalMsgBody;
+
+	  Burn notification is sent to master jetton( minter ) contract  on successfull burn
+
+	  1) Message dst addr should be equal to master jetton addr
+	  2) query_id should be equal to request query_id
+	  3) amount should be equal to request burn_amount
+	  4) response_destination should be equal to request resp_dst
+
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;; Burn notification should be sent to master
+	throw_unless( 901, equal_slices( get_master(), parsed_msg.at( 4 ) ) );
+
+	slice msg_body	 = parsed_msg.at( 8 );
+
+	throw_unless( 902, op_burn_notification  == msg_body~load_uint( 32 ) );
+
+	throw_unless( 903, query_id == msg_body~load_uint( 64 ) );
+
+	throw_unless( 903, burn_amount == msg_body~load_coins() );
+
+	throw_unless( 904, equal_slices( sender, msg_body~load_msg_addr() ) );
+
+	throw_unless( 905, equal_slices( resp_dst, msg_body~load_msg_addr() ) );
+}
+
+_ verify_excess_jetton( int query_id, slice resp_dst, int msg_value, int forward_fee, int forward_amount, cell msg ) impure inline {
+	{-
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+	Excess message should be sent to resp_dst with all of the msg_value - fees taken to process
+	We verify that:
+	1) message is sent to resp_dst
+	2) attached amount is at least msg_value - forward_fee * 2
+	3) op matches excess op
+	4) query_id matches request query_id
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;;Check dst_addr to be equal to resp_dst
+	throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) );
+
+	int total_sent  = parsed_msg.at( 5 );
+	int should_sent = msg_value - forward_amount - forward_fee * 2;
+
+	throw_unless( 702, total_sent >= should_sent );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) );
+
+	throw_unless( 704, query_id == msg_body~load_uint( 64 ) );
+}
+
+int transfer_test_msg_value( int msg_value, int forward_fee, int forward_amount, int expect_fail? ) impure inline {
+	{-
+	 Transfer should be rejected if wallet has less  or equal to
+	 forward_amount + number of forwarded messages ( 1 if no forward_amount 2 otherwise ) * fwd_fee + jetton_gas_fee + jetton_min_storage 
+	 on the balance.
+	 Tricky part is that forward and response part happens on the receiver wallet.
+	 So jetton_gas_fee should be taken twice by sender and the receiver + one min_storage on the receiver part
+	 Thus IMHO at least one jetton_gas_fee should be accounted from sender balance and not msg_value.
+	 However author if this contract comparse all of that vs msg_value that would be passed to the receiver
+	 https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-wallet.fc#L80
+	-}
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int gas_used	    = 0;
+	int transfer_amount = get_test_balance();
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	if( expect_fail? ) {
+		;; Should fail with no actions
+		gas_used 	    = invoke_method_expect_fail( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+		assert_no_actions();
+	}
+	else {
+
+		var ( gas_used, _ ) = invoke_method( recv_internal, [one_unit, msg_value + jetton_min_storage, msg, msg_body.end_cell().begin_parse() ] );
+	}
+
+	return gas_used;
+}
+
+{-
+  Testing wallet transfer query handling
+  
+  transfer#0f8a7ea5 query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
+                 response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+                 forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+                 = InternalMsgBody;
+
+-}
+
+int __test_transfer_not_owner() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice non_owner  = gen_non_owner( owner );
+	int   query_id   = rand( 1337 ) + 1;
+	builder msg_body = generate_jetton_transfer_request( query_id, one_unit, dst,resp_dst, null(), 0, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err	 = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;;Now verify that owner addr triggered fail by changing source to owner addr
+
+	msg		 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_transfer_no_jettons() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance = get_test_balance();
+	int transfer_amount = balance * 10; ;; Going to ask for way more jettons
+
+
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), 0, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err	 = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Now let's verify that balance triggers fail by requesting transfer equal to balance amount
+
+	msg_body 	 = generate_jetton_transfer_request( query_id, balance, dst,resp_dst, null(), 0, null(), false );
+
+	msg	 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_transfer_storage_fee() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + forward_amount + excesses; ;;Account for everythin except storage fee 
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + jetton_min_storage, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+
+int __test_transfer_jetton_gas_fee() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = forward_fee * msg_expected + forward_amount + excesses; ;;Account for everythin except transfer gas fee
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + transfer_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_fee_once() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = 0; ;;one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_amount + excesses; ;;Account for everythin except forward_fee;
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_fee_twice() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee  + forward_amount + excesses; ;;Account for everythin except that two forward fees instead of one
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_amount() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + excesses; ;;Account for everythin except forward_amount
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_amount, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_excess() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + forward_amount; ;;Account for everythin except excess ( nothing to return )
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + excesses, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int verify_successfull_transfer( int transfer_amount, int forward_amount, cell fwd_payload ) impure inline {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int forward_fee	    = one_unit / 10;
+	int msg_value	    = one_unit * 2;
+	int prev_balance    = get_test_balance();
+	int expect_messages = forward_amount > 0 ? 2 : 1;
+
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, fwd_payload, false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	;; Should fail with no actions
+	var ( gas_send, _ ) = invoke_method( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Here we should get a message sent.
+
+	tuple actions	 = parse_c5();
+
+	throw_unless( 500, actions.tuple_length() == 1 );
+
+	( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple();
+
+	throw_unless( 501, action_type == 0 );
+	;; And we should verify that wallet balance decreased
+
+	throw_unless( 502, prev_balance > get_test_balance() );
+
+	{-
+	  Now here comes the tricky part.
+	  We should verify that receiver wallet would send certain messages back to dst and resp_dst
+	  At a first glace receiver wallet is deployed from this contract code so we can just relay those messages 
+	  to the recv_internal of this contract.
+	  However, data in c4 is different from one deployed contract to another.
+	  So let's try to pick data from StateInit and set it to this contract c4 to make sure that it's on the same page
+	  with hypothetical receiver.
+	-}
+
+	cell old_data = get_data(); ;; Backup old c4 data
+
+
+	tuple parsed_msg   = unsafe_tuple( parse_internal_message( sent_msg ) );
+	tuple state_init   = parsed_msg.at( 7 );
+	throw_if( 503, null?( state_init ) );
+
+	;;Fourth element of StateInit is data cell
+
+	cell sent_data	   = state_init.get_state_init_field( 4 );
+	throw_if( 504, null?( sent_data ) );
+
+	set_data( sent_data ); ;;Now we got same persistent state the receiver would in real world
+	slice sent_body    = parsed_msg.at( 8 ); ;;Getting parsed msg body
+
+	{-
+	 Now we're going to have issues with the source address of the message because in tesing mode it is not going to pass 
+	 calculate_user_jetton_wallet_address check
+	 So let's switch it up with the jetton master address
+	 Expect successfull execution
+	 and up to two messages.
+	 1) if forward_amount > 0 also transfer notification
+
+	 		transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) 
+                              sender:MsgAddress forward_payload:(Either Cell ^Cell)
+                              = InternalMsgBody;
+
+	 2) excesses#d53276db query_id:uint64 = InternalMsgBody;
+	-}
+
+	sent_msg~replace_msg_source( get_master() );
+	;;Let's say balance was empty before
+	var ( gas_replay, _ ) = invoke_method( recv_internal, [ msg_value - forward_fee, msg_value - forward_fee, sent_msg, sent_body ] );
+	actions 	      = parse_c5();
+	throw_unless( 505, actions.tuple_length() == expect_messages );
+
+	if( forward_amount > 0 ) {
+		
+		( action_type, cell notify_msg, mode ) = actions.at(0).untriple();
+		throw_unless( 501, action_type == 0 );
+
+		verify_transfer_notification( notify_msg, transfer_amount, query_id, dst, resp_dst, owner, forward_amount, fwd_payload );
+	}
+
+	( action_type, cell excess_msg, mode ) = actions.at( expect_messages - 1 ).untriple();
+
+	throw_unless( 501, action_type == 0 );
+
+	verify_excess_jetton( query_id, resp_dst, msg_value - forward_fee, forward_fee, forward_amount, excess_msg );
+	
+	return gas_send + gas_replay;
+}
+
+int __test_transfer_success_no_fwd() {
+	int gas_used = verify_successfull_transfer( one_unit, 0, null() ); ;;Test without forward amount first
+
+	return gas_used;
+}
+
+int __test_transfer_success_with_fwd() {
+
+	cell payload = begin_cell().store_slice("Hop hey").end_cell();
+	;; 0.25 forward amount and some payload
+	int gas_used = verify_successfull_transfer( one_unit, one_unit / 4, payload ); ;;Test with forward amount
+	return gas_used;
+}
+
+{- 
+	Testing burn request
+
+	burn#595f07bc query_id:uint64 amount:(VarUInteger 16)
+              response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+              = InternalMsgBody;
+
+-}
+
+int burn_test_msg_value( int msg_value, int forward_fee, int expect_fail? ) impure inline {
+	;; Testing msg_value related cases
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+	int  gas_used  = 0;
+	var  msg_body  = generate_jetton_burn_request( query_id, get_test_balance() / 10, resp_dst, null() ); 
+	cell msg       = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+	slice ms       = msg_body.end_cell().begin_parse();
+
+	if( expect_fail? ) {
+		gas_used = invoke_method_expect_fail( recv_internal, [ msg_value, msg_value, msg, ms ] );
+	}
+	else {
+		(gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, ms ] );
+	}
+	
+	return gas_used;	
+}
+
+int __test_burn_not_owner() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice non_owner = gen_non_owner( owner );
+	var   msg_body  = generate_jetton_burn_request( query_id, one_unit * 5, resp_dst, null() ); ;;Burn 5 jetton units
+	cell  msg	= generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 );
+	int   msg_value = one_unit;
+
+	;; Should fail with no actions
+	int gas_err	= invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; changing msg source addres to owner to verify error trigger
+	msg		= generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_burn_too_many() {
+	;; Testing case where user burns more jettons than available on balance
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance    = get_test_balance();
+	var  msg_body  = generate_jetton_burn_request( query_id, balance + 1, resp_dst, null() ); ;;Burn balance + 1 jettons
+	cell msg       = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err    = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Now transfer exactly balance to verify error trigger
+	msg_body       = generate_jetton_burn_request( query_id, balance, resp_dst, null() ); 
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_burn_no_gas() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = forward_fee + excess; ;;Only forward fee and access no gas fee
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + burn_fee, forward_fee, false );
+
+	return gas_used;
+
+}
+
+int __test_burn_no_forward_fee() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = burn_fee + excess; ;;Only burn_fee and excess no forward_fee
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + forward_fee, forward_fee, false );
+
+	return gas_used;
+}
+
+int __test_burn_no_excess() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = burn_fee + forward_fee; ;;Only burn_fee + forward_fee nothing to return back
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + excess, forward_fee, false );
+
+	return gas_used;
+}
+
+int __test_burn_successfull() {
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance         = get_test_balance();
+	int msg_value       = one_unit;
+	int forward_fee     = one_unit / 10;
+	int forward_amount  = one_unit / 4;
+	int burn_amount     = ( rand( 10 ) + 1 ) * balance / 10;
+
+	var  msg_body       = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() );
+	cell msg            = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+	var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Balance should decrease by burn amount
+	throw_unless( 502, balance - burn_amount == get_test_balance() );
+
+	;; We expect excess message to be sent to resp_dst
+	tuple actions  = parse_c5();
+	
+	throw_unless( 505, actions.tuple_length() == 1 );
+
+	( int action_type, cell burn_note, int mode ) = actions.first().untriple();
+	throw_unless( 501, action_type == 0 );
+
+	verify_burn_notification( burn_note, query_id, burn_amount, owner, resp_dst );
+
+	return gas_used;
+
+}
+
+int __test_internal_transfer() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice from          = get_master();
+	int balance         = get_test_balance();
+	int forward_ton     = one_unit / 10;
+	int msg_value       = one_unit;
+	int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit
+
+
+	var  msg_body       = generate_jetton_internal_transfer_request( query_id, transfer_amount, from, dst, forward_ton, null(), false );
+	cell msg            = generate_internal_message_custom( 0, 0, 0,  msg_body, from, null(), 0 );
+	var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	throw_unless( 500, balance + transfer_amount == get_test_balance() );
+
+	return gas_used;
+}
+
+;; Get methods testing starts here
+
+int __test_get_wallet_data() {
+	var ( balance, owner, master, code ) = load_test_data();
+	var ( gas_used, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 300, stack.tuple_length() == 4 );
+	throw_unless( 301, balance == stack.first() );
+	throw_unless( 302, equal_slices( owner, stack.second() ) );
+	throw_unless( 303, equal_slices( master, stack.third() ) );
+	throw_unless( 304, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+	return gas_used;
+}
diff --git a/src/toncli/projects/jetton_wallet/tests/.gitkeep b/src/toncli/projects/jetton_wallet/tests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/jetton_wallet/tests/utils/constants.func b/src/toncli/projects/jetton_wallet/tests/utils/constants.func
new file mode 100644
index 0000000..28221d7
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/tests/utils/constants.func
@@ -0,0 +1,7 @@
+const int one_unit = 1000000000; ;; 10^9
+
+;; These are not standardized values each Jetton can have it's own fee guidelines
+const int jetton_min_storage = 10000000; ;;0.01 TON
+const int jetton_gas_fee     = 10000000; ;;0.01 TON
+
+
diff --git a/src/toncli/projects/jetton_wallet/tests/wallet-tests-int.func b/src/toncli/projects/jetton_wallet/tests/wallet-tests-int.func
new file mode 100644
index 0000000..1e509c7
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/tests/wallet-tests-int.func
@@ -0,0 +1,118 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+
+int __test_get_wallet_data_transfer() {
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance  = stack.first();
+	slice owner  = stack.second();
+	slice master = stack.third();
+	cell code    = stack.fourth();
+
+	;; Nothing to send
+
+	if( balance <= 0 ) {
+		return gas_before; ;;throw( 100 ) ?
+	}
+
+	int transfer_amount      = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance
+	int forward_amount       = one_unit / 10;
+	int forward_fee          = one_unit / 100;
+
+
+	builder msg_body         = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false );
+	cell    msg	             = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	int new_balance = stack.first();
+
+	throw_unless( 500, balance - transfer_amount == stack.first() );
+
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+
+	return gas_before + gas_transfer + gas_after;
+}
+
+int __test_get_wallet_internal_transfer() {
+
+	{-
+	 When incoming transfer happens
+	 Balance should increase accordingly
+	-}
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance         = stack.first();
+	slice owner         = stack.second();
+	slice master        = stack.third();
+	cell code           = stack.fourth();
+
+
+	int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit
+	int forward_ton     = one_unit / 10;
+	int msg_value       = one_unit;
+
+	var  msg_body       = generate_jetton_internal_transfer_request( query_id, transfer_amount, master, dst, forward_ton, null(), false );
+	cell msg            = generate_internal_message_custom( 0, 0, 0,  msg_body, master, null(), 0 );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 500, balance + transfer_amount == stack.first() );
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+	return gas_before + gas_transfer + gas_after;
+}
+
+int __test_get_wallet_data_burn() {
+
+	var ( _, dst, resp_dst, query_id ) = setup_req_fields();
+	var ( gas_before, stack )          = invoke_method( get_wallet_data, [] );
+
+	int balance  = stack.first();
+	slice owner  = stack.second();
+	slice master = stack.third();
+	cell code    = stack.fourth();
+
+
+	;; Nothing to burn
+
+	if( balance <= 0 ) {
+		return gas_before; ;;throw( 100 ) ?
+	}
+
+	int burn_amount      = ( rand( 10 ) + 1 ) * balance / 10; ;; From 0.1 * balance to 1 * balance
+	int forward_amount   = one_unit / 10;
+	int forward_fee      = one_unit / 100;
+	var  msg_body        = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() );
+	cell msg             = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_burn, _ )  = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	var ( gas_after, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 500, balance - burn_amount == stack.first() );
+
+	;; Rest should not change
+	throw_unless( 501, equal_slices( owner, stack.second() ) );
+	throw_unless( 502, equal_slices( master, stack.third() ) );
+	throw_unless( 503, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+
+
+	return gas_before + gas_burn;
+}
diff --git a/src/toncli/projects/jetton_wallet/tests/wallet-tests.func b/src/toncli/projects/jetton_wallet/tests/wallet-tests.func
new file mode 100644
index 0000000..8c38e83
--- /dev/null
+++ b/src/toncli/projects/jetton_wallet/tests/wallet-tests.func
@@ -0,0 +1,665 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+
+( int, slice, slice, cell ) load_test_data() inline {
+
+	slice ds = get_data().begin_parse();
+	return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref());
+}
+
+( slice ) get_owner() inline {
+
+	slice ds = get_data().begin_parse();
+	ds~load_coins();
+
+	return ds~load_msg_addr();
+}
+
+( slice ) get_master() inline {
+	slice ds = get_data().begin_parse();
+	ds~load_coins();
+	ds~load_msg_addr();
+	return ds~load_msg_addr();
+}
+
+( int )  get_test_balance() inline {
+	slice ds = get_data().begin_parse();
+
+	return ds~load_coins();
+}
+
+( cell,() ) replace_msg_source( cell msg, slice new_addr ) inline {
+	slice   old_msg = msg.begin_parse();
+	builder new_msg = begin_cell().store_slice( old_msg~load_bits( 4 ) );
+	old_msg~load_msg_addr(); ;;Skip source addr
+	new_msg = new_msg.store_slice( new_addr ).store_slice( old_msg ); ;;Store new addr and the rest of the orig msg
+
+	return ( new_msg.end_cell(),() );
+	
+}
+
+slice gen_non_owner( slice old_owner ) inline {
+	slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	;; In theory we can win a bingo and get same 256bit integer from RNG
+	while( equal_slices( new_owner, old_owner ) ) {
+		new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+	}
+
+	return new_owner;
+}
+
+( slice, slice, slice, int ) setup_req_fields() {
+	slice owner    = get_owner();
+	slice dst      = generate_internal_address_with_custom_data( 0, 0, random() );
+	slice resp_dst = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	return ( owner, dst, resp_dst, rand( 1337 ) + 1 );
+}
+
+cell get_state_init_field( tuple state_init, int idx ) inline {
+	cell res = null();
+	;; Next flag index
+	int next_idx = 0;
+
+	do {
+		int flag = state_init.at( next_idx ); 
+
+		next_idx = flag ? next_idx + 2 : next_idx + 1;
+		idx -= 1;
+
+		if( idx == 0 & flag ) {
+			res = state_init.at( next_idx - 1 );
+		}
+
+	} until( ~ res.null?() | idx <= 0 )
+
+	return res;
+}
+
+_ verify_transfer_notification( cell msg, int jetton_amount, int query_id, slice dst, slice resp_dst, slice sender, int forward_amount, cell payload  ) impure inline {
+	{-
+	transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) 
+                              sender:MsgAddress forward_payload:(Either Cell ^Cell)
+                              = InternalMsgBody;
+
+	Transfer notification message is sent when forward_amount > 0 from receiver to sender wallet
+	1) query_id should be equal to the query_id of sender request
+	2) jetton_amount should be equal to the amount of transfered jettons
+	3) sender should be equal to the sender address
+	4) forward_payload ( if any ) should be equal to the payload of sender request
+	-}
+
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	{-
+	 Why it's dst and ton resp_dst? 
+	 I don't fully get it, but by standard:
+	 if forward_amount > 0 ensure that receiver's jetton-wallet send message to destination address with forward_amount nanotons attached
+	 Probably has something to do with separating excess and forward amount value
+	-}
+	throw_unless( 801, equal_slices( dst, parsed_msg.at( 4 ) ) );
+
+	;;forward_amount tons should be attached
+	throw_unless( 802, forward_amount == parsed_msg.at( 5 )  );
+
+	slice msg_body	= parsed_msg.at( 8 );
+
+	throw_unless( 803, op_transfer_notification == msg_body~load_uint( 32 ) );
+
+	throw_unless( 804, query_id == msg_body~load_uint( 64 ) );
+
+	throw_unless( 805, jetton_amount == msg_body~load_coins() );
+
+	throw_unless( 806, equal_slices( sender, msg_body~load_msg_addr() ) );
+
+	if( ~ null?( payload ) ) {
+		if( msg_body~load_uint( 1 ) ) {
+			throw_unless( 807, equal_slices( payload.begin_parse(), msg_body~load_ref().begin_parse() ) );
+		}
+		else {
+			throw_unless( 807, equal_slices( payload.begin_parse(), msg_body ) );
+		}
+	}
+}
+
+_ verify_burn_notification( cell msg, int query_id, int burn_amount, slice sender, slice resp_dst ) impure inline {
+	{-
+	  burn_notification query_id:uint64 amount:(VarUInteger 16)
+	       sender:MsgAddress response_destination:MsgAddress
+	       = InternalMsgBody;
+
+	  Burn notification is sent to master jetton( minter ) contract  on successfull burn
+
+	  1) Message dst addr should be equal to master jetton addr
+	  2) query_id should be equal to request query_id
+	  3) amount should be equal to request burn_amount
+	  4) response_destination should be equal to request resp_dst
+
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;; Burn notification should be sent to master
+	throw_unless( 901, equal_slices( get_master(), parsed_msg.at( 4 ) ) );
+
+	slice msg_body	 = parsed_msg.at( 8 );
+
+	throw_unless( 902, op_burn_notification  == msg_body~load_uint( 32 ) );
+
+	throw_unless( 903, query_id == msg_body~load_uint( 64 ) );
+
+	throw_unless( 903, burn_amount == msg_body~load_coins() );
+
+	throw_unless( 904, equal_slices( sender, msg_body~load_msg_addr() ) );
+
+	throw_unless( 905, equal_slices( resp_dst, msg_body~load_msg_addr() ) );
+}
+
+_ verify_excess_jetton( int query_id, slice resp_dst, int msg_value, int forward_fee, int forward_amount, cell msg ) impure inline {
+	{-
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+	Excess message should be sent to resp_dst with all of the msg_value - fees taken to process
+	We verify that:
+	1) message is sent to resp_dst
+	2) attached amount is at least msg_value - forward_fee * 2
+	3) op matches excess op
+	4) query_id matches request query_id
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;;Check dst_addr to be equal to resp_dst
+	throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) );
+
+	int total_sent  = parsed_msg.at( 5 );
+	int should_sent = msg_value - forward_amount - forward_fee * 2;
+
+	throw_unless( 702, total_sent >= should_sent );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) );
+
+	throw_unless( 704, query_id == msg_body~load_uint( 64 ) );
+}
+
+int transfer_test_msg_value( int msg_value, int forward_fee, int forward_amount, int expect_fail? ) impure inline {
+	{-
+	 Transfer should be rejected if wallet has less  or equal to
+	 forward_amount + number of forwarded messages ( 1 if no forward_amount 2 otherwise ) * fwd_fee + jetton_gas_fee + jetton_min_storage 
+	 on the balance.
+	 Tricky part is that forward and response part happens on the receiver wallet.
+	 So jetton_gas_fee should be taken twice by sender and the receiver + one min_storage on the receiver part
+	 Thus IMHO at least one jetton_gas_fee should be accounted from sender balance and not msg_value.
+	 However author if this contract comparse all of that vs msg_value that would be passed to the receiver
+	 https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-wallet.fc#L80
+	-}
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int gas_used	    = 0;
+	int transfer_amount = get_test_balance();
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	if( expect_fail? ) {
+		;; Should fail with no actions
+		gas_used 	    = invoke_method_expect_fail( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+		assert_no_actions();
+	}
+	else {
+
+		var ( gas_used, _ ) = invoke_method( recv_internal, [one_unit, msg_value + jetton_min_storage, msg, msg_body.end_cell().begin_parse() ] );
+	}
+
+	return gas_used;
+}
+
+{-
+  Testing wallet transfer query handling
+  
+  transfer#0f8a7ea5 query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress
+                 response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+                 forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell)
+                 = InternalMsgBody;
+
+-}
+
+int __test_transfer_not_owner() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice non_owner  = gen_non_owner( owner );
+	int   query_id   = rand( 1337 ) + 1;
+	builder msg_body = generate_jetton_transfer_request( query_id, one_unit, dst,resp_dst, null(), 0, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err	 = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;;Now verify that owner addr triggered fail by changing source to owner addr
+
+	msg		 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_transfer_no_jettons() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance = get_test_balance();
+	int transfer_amount = balance * 10; ;; Going to ask for way more jettons
+
+
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), 0, null(), false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err	 = invoke_method_expect_fail( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Now let's verify that balance triggers fail by requesting transfer equal to balance amount
+
+	msg_body 	 = generate_jetton_transfer_request( query_id, balance, dst,resp_dst, null(), 0, null(), false );
+
+	msg	 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit * 10, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_transfer_storage_fee() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + forward_amount + excesses; ;;Account for everythin except storage fee 
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + jetton_min_storage, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+
+int __test_transfer_jetton_gas_fee() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = forward_fee * msg_expected + forward_amount + excesses; ;;Account for everythin except transfer gas fee
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + transfer_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_fee_once() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = 0; ;;one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_amount + excesses; ;;Account for everythin except forward_fee;
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_fee_twice() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee  + forward_amount + excesses; ;;Account for everythin except that two forward fees instead of one
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_fee, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_forward_amount() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + excesses; ;;Account for everythin except forward_amount
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + forward_amount, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int __test_transfer_excess() {
+	int transfer_fee   = jetton_gas_fee * 2;
+	int forward_fee    = one_unit / 10;
+	int forward_amount = one_unit / 4;
+	int excesses	   = 1;
+	int msg_expected   = forward_amount > 0 ? 2 : 1;
+	int msg_value      = transfer_fee + forward_fee * msg_expected + forward_amount; ;;Account for everythin except excess ( nothing to return )
+	int gas_used	   = 0;
+
+	;;Expect fail
+	gas_used 	  += transfer_test_msg_value( msg_value, forward_fee, forward_amount, true );
+
+	;; Verifying error trigger
+	gas_used	  += transfer_test_msg_value( msg_value + excesses, forward_fee, forward_amount, false ) ;
+
+	return gas_used;
+}
+
+int verify_successfull_transfer( int transfer_amount, int forward_amount, cell fwd_payload ) impure inline {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int forward_fee	    = one_unit / 10;
+	int msg_value	    = one_unit * 2;
+	int prev_balance    = get_test_balance();
+	int expect_messages = forward_amount > 0 ? 2 : 1;
+
+	builder msg_body = generate_jetton_transfer_request( query_id, transfer_amount, dst,resp_dst, null(), forward_amount, fwd_payload, false );
+	cell    msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+
+	;; Should fail with no actions
+	var ( gas_send, _ ) = invoke_method( recv_internal, [ one_unit, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Here we should get a message sent.
+
+	tuple actions	 = parse_c5();
+
+	throw_unless( 500, actions.tuple_length() == 1 );
+
+	( int action_type, cell sent_msg, int mode ) = actions.at(0).untriple();
+
+	throw_unless( 501, action_type == 0 );
+	;; And we should verify that wallet balance decreased
+
+	throw_unless( 502, prev_balance > get_test_balance() );
+
+	{-
+	  Now here comes the tricky part.
+	  We should verify that receiver wallet would send certain messages back to dst and resp_dst
+	  At a first glace receiver wallet is deployed from this contract code so we can just relay those messages 
+	  to the recv_internal of this contract.
+	  However, data in c4 is different from one deployed contract to another.
+	  So let's try to pick data from StateInit and set it to this contract c4 to make sure that it's on the same page
+	  with hypothetical receiver.
+	-}
+
+	cell old_data = get_data(); ;; Backup old c4 data
+
+
+	tuple parsed_msg   = unsafe_tuple( parse_internal_message( sent_msg ) );
+	tuple state_init   = parsed_msg.at( 7 );
+	throw_if( 503, null?( state_init ) );
+
+	;;Fourth element of StateInit is data cell
+
+	cell sent_data	   = state_init.get_state_init_field( 4 );
+	throw_if( 504, null?( sent_data ) );
+
+	set_data( sent_data ); ;;Now we got same persistent state the receiver would in real world
+	slice sent_body    = parsed_msg.at( 8 ); ;;Getting parsed msg body
+
+	{-
+	 Now we're going to have issues with the source address of the message because in tesing mode it is not going to pass 
+	 calculate_user_jetton_wallet_address check
+	 So let's switch it up with the jetton master address
+	 Expect successfull execution
+	 and up to two messages.
+	 1) if forward_amount > 0 also transfer notification
+
+	 		transfer_notification#7362d09c query_id:uint64 amount:(VarUInteger 16) 
+                              sender:MsgAddress forward_payload:(Either Cell ^Cell)
+                              = InternalMsgBody;
+
+	 2) excesses#d53276db query_id:uint64 = InternalMsgBody;
+	-}
+
+	sent_msg~replace_msg_source( get_master() );
+	;;Let's say balance was empty before
+	var ( gas_replay, _ ) = invoke_method( recv_internal, [ msg_value - forward_fee, msg_value - forward_fee, sent_msg, sent_body ] );
+	actions 	      = parse_c5();
+	throw_unless( 505, actions.tuple_length() == expect_messages );
+
+	if( forward_amount > 0 ) {
+		
+		( action_type, cell notify_msg, mode ) = actions.at(0).untriple();
+		throw_unless( 501, action_type == 0 );
+
+		verify_transfer_notification( notify_msg, transfer_amount, query_id, dst, resp_dst, owner, forward_amount, fwd_payload );
+	}
+
+	( action_type, cell excess_msg, mode ) = actions.at( expect_messages - 1 ).untriple();
+
+	throw_unless( 501, action_type == 0 );
+
+	verify_excess_jetton( query_id, resp_dst, msg_value - forward_fee, forward_fee, forward_amount, excess_msg );
+	
+	return gas_send + gas_replay;
+}
+
+int __test_transfer_success_no_fwd() {
+	int gas_used = verify_successfull_transfer( one_unit, 0, null() ); ;;Test without forward amount first
+
+	return gas_used;
+}
+
+int __test_transfer_success_with_fwd() {
+
+	cell payload = begin_cell().store_slice("Hop hey").end_cell();
+	;; 0.25 forward amount and some payload
+	int gas_used = verify_successfull_transfer( one_unit, one_unit / 4, payload ); ;;Test with forward amount
+	return gas_used;
+}
+
+{- 
+	Testing burn request
+
+	burn#595f07bc query_id:uint64 amount:(VarUInteger 16)
+              response_destination:MsgAddress custom_payload:(Maybe ^Cell)
+              = InternalMsgBody;
+
+-}
+
+int burn_test_msg_value( int msg_value, int forward_fee, int expect_fail? ) impure inline {
+	;; Testing msg_value related cases
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+	int  gas_used  = 0;
+	var  msg_body  = generate_jetton_burn_request( query_id, get_test_balance() / 10, resp_dst, null() ); 
+	cell msg       = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), forward_fee );
+	slice ms       = msg_body.end_cell().begin_parse();
+
+	if( expect_fail? ) {
+		gas_used = invoke_method_expect_fail( recv_internal, [ msg_value, msg_value, msg, ms ] );
+	}
+	else {
+		(gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, ms ] );
+	}
+	
+	return gas_used;	
+}
+
+int __test_burn_not_owner() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice non_owner = gen_non_owner( owner );
+	var   msg_body  = generate_jetton_burn_request( query_id, one_unit * 5, resp_dst, null() ); ;;Burn 5 jetton units
+	cell  msg	= generate_internal_message_custom( 0, 0, 0, msg_body, non_owner, null(), 0 );
+	int   msg_value = one_unit;
+
+	;; Should fail with no actions
+	int gas_err	= invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; changing msg source addres to owner to verify error trigger
+	msg		= generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_burn_too_many() {
+	;; Testing case where user burns more jettons than available on balance
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance    = get_test_balance();
+	var  msg_body  = generate_jetton_burn_request( query_id, balance + 1, resp_dst, null() ); ;;Burn balance + 1 jettons
+	cell msg       = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	;; Should fail with no actions
+	int gas_err    = invoke_method_expect_fail( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Now transfer exactly balance to verify error trigger
+	msg_body       = generate_jetton_burn_request( query_id, balance, resp_dst, null() ); 
+
+	var ( gas_success, _ ) = invoke_method( recv_internal, [ one_unit, one_unit, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_err + gas_success;
+}
+
+int __test_burn_no_gas() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = forward_fee + excess; ;;Only forward fee and access no gas fee
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + burn_fee, forward_fee, false );
+
+	return gas_used;
+
+}
+
+int __test_burn_no_forward_fee() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = burn_fee + excess; ;;Only burn_fee and excess no forward_fee
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + forward_fee, forward_fee, false );
+
+	return gas_used;
+}
+
+int __test_burn_no_excess() {
+	int burn_fee = jetton_gas_fee * 2;
+	int forward_fee = one_unit / 10;
+	int excess 	= 1;
+	int msg_value   = burn_fee + forward_fee; ;;Only burn_fee + forward_fee nothing to return back
+	int gas_used = burn_test_msg_value( msg_value, forward_fee, true );
+	assert_no_actions();
+	;; verifying error trigger
+	gas_used    += burn_test_msg_value( msg_value + excess, forward_fee, false );
+
+	return gas_used;
+}
+
+int __test_burn_successfull() {
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	int balance         = get_test_balance();
+	int msg_value       = one_unit;
+	int forward_fee     = one_unit / 10;
+	int forward_amount  = one_unit / 4;
+	int burn_amount     = ( rand( 10 ) + 1 ) * balance / 10;
+
+	var  msg_body       = generate_jetton_burn_request( query_id, burn_amount, resp_dst, null() );
+	cell msg            = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+	var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Balance should decrease by burn amount
+	throw_unless( 502, balance - burn_amount == get_test_balance() );
+
+	;; We expect excess message to be sent to resp_dst
+	tuple actions  = parse_c5();
+	
+	throw_unless( 505, actions.tuple_length() == 1 );
+
+	( int action_type, cell burn_note, int mode ) = actions.first().untriple();
+	throw_unless( 501, action_type == 0 );
+
+	verify_burn_notification( burn_note, query_id, burn_amount, owner, resp_dst );
+
+	return gas_used;
+
+}
+
+int __test_internal_transfer() {
+
+	var ( owner, dst, resp_dst, query_id ) = setup_req_fields();
+
+	slice from          = get_master();
+	int balance         = get_test_balance();
+	int forward_ton     = one_unit / 10;
+	int msg_value       = one_unit;
+	int transfer_amount = ( rand( 10 ) + 1 ) * one_unit / 10; ;;From 0.1 to 1 unit
+
+
+	var  msg_body       = generate_jetton_internal_transfer_request( query_id, transfer_amount, from, dst, forward_ton, null(), false );
+	cell msg            = generate_internal_message_custom( 0, 0, 0,  msg_body, from, null(), 0 );
+	var ( gas_used, _ ) = invoke_method( recv_internal, [ msg_value, msg_value, msg, msg_body.end_cell().begin_parse() ] );
+
+	throw_unless( 500, balance + transfer_amount == get_test_balance() );
+
+	return gas_used;
+}
+
+;; Get methods testing starts here
+
+int __test_get_wallet_data() {
+	var ( balance, owner, master, code ) = load_test_data();
+	var ( gas_used, stack ) = invoke_method( get_wallet_data, [] );
+
+	throw_unless( 300, stack.tuple_length() == 4 );
+	throw_unless( 301, balance == stack.first() );
+	throw_unless( 302, equal_slices( owner, stack.second() ) );
+	throw_unless( 303, equal_slices( master, stack.third() ) );
+	throw_unless( 304, equal_slices( code.begin_parse(), stack.fourth().begin_parse() ) );
+
+	return gas_used;
+}
diff --git a/src/toncli/projects/nft_collection/tests/.gitkeep b/src/toncli/projects/nft_collection/tests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_collection/tests/collection-tests-int.func b/src/toncli/projects/nft_collection/tests/collection-tests-int.func
new file mode 100644
index 0000000..c6727c7
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/collection-tests-int.func
@@ -0,0 +1,94 @@
+#pragma version >=0.2.0;
+
+#include "utils/collection-data.func";
+#include "utils/constants.func";
+
+
+int __test_get_collection_data_deploy() {
+	var ( gas_before, stack ) = invoke_method( get_collection_data, [] );
+
+	int idx_next       = stack.first();
+	int forward_amount = one_ton / 10;
+	int query_id       = rand( 1337 ) + 1;
+	cell coll_content  = stack.second();
+	slice owner        = stack.third();
+
+	cell nft_content   = begin_cell().store_slice("my_nft.json").end_cell();
+	cell nft_init      = begin_cell().store_slice(owner).store_ref(nft_content).end_cell();
+
+	builder msg_body   = generate_nft_deploy_request( idx_next, nft_init, query_id, forward_amount);
+	cell msg           = generate_internal_message_custom(0, 0, 0, msg_body, owner, null(), 0);
+	
+	var ( gas_deploy, _ )    = invoke_method(recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()]);
+	var ( gas_after, stack ) = invoke_method( get_collection_data, [] );
+	;;Index should increase by one
+	throw_unless( 500, stack.first() == idx_next + 1 );
+
+	;; Nothing should change here
+	throw_unless( 501, equal_slices( coll_content.begin_parse(), stack.second().begin_parse() ) );
+	throw_unless( 502, equal_slices( owner, stack.third() ) );
+
+	return gas_before + gas_after + gas_deploy;
+}
+
+int __test_get_collection_data_change_owner() {
+
+	var ( gas_before, stack ) = invoke_method( get_collection_data, [] );
+
+	int query_id       = rand( 1337 ) + 1;
+	int change_owner   = 3; ;;WARNING contract dependent value.
+	slice owner        = stack.third();
+	slice new_owner    = generate_internal_address_with_custom_data(0, 0, random());
+	builder msg_body   = generate_internal_message_body( change_owner, query_id ).store_slice( new_owner );
+	cell    msg        = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_change, _ )    = invoke_method( recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()] );
+	var ( gas_after, stack ) = invoke_method( get_collection_data, [] );
+
+	throw_unless( 500, equal_slices( new_owner, stack.third() ) );
+
+	return gas_before + gas_change + gas_after;
+}
+
+int __test_get_nft_address_by_index() {
+
+    slice owner = get_owner();
+
+    ;; To test that nft address is correct
+    ;; we have to deploy one first and capture it's address
+    ;; Index:0
+
+    cell nft_content = begin_cell().store_slice("my_nft.json").end_cell();
+    cell nft_init    = begin_cell().store_slice(owner).store_ref(nft_content).end_cell();
+
+    int query_id = rand(1337) + 1;
+    int forward_amount = one_ton / 10;
+    int deploy_idx = 0;
+
+    builder msg_body = generate_nft_deploy_request(deploy_idx, nft_init, query_id, forward_amount);
+    cell msg = generate_internal_message_custom(0, 0, 0, msg_body, owner, null(), 0);
+
+    (int gas_used, _) = invoke_method(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]);
+    ;; Should successfully deploy and produce singe deployment message
+
+    tuple actions = parse_c5();
+    throw_unless(500, actions.tuple_length() == 1);
+
+    (int action_type, cell body, int mode) = actions.at(0).untriple();
+    throw_unless(501, action_type == 0);
+
+    tuple parsed_msg = unsafe_tuple(parse_internal_message(body));
+
+    ;;Picking nft address from deployment message
+    slice nft_address = parsed_msg.at(4);
+
+    var (gas_used2, stack) = invoke_method(get_nft_address_by_index, [deploy_idx]);
+
+    throw_unless(502, stack.tuple_length() == 1);
+
+    throw_unless(503, equal_slices(nft_address, stack.first()));
+
+
+    return gas_used;
+}
+
diff --git a/src/toncli/projects/nft_collection/tests/collection-tests.func b/src/toncli/projects/nft_collection/tests/collection-tests.func
new file mode 100644
index 0000000..f30d1fa
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/collection-tests.func
@@ -0,0 +1,272 @@
+#pragma version >=0.2.0;
+
+#include "utils/collection-data.func";
+#include "utils/constants.func";
+
+
+;; Pretty unpleasent tuple to work with.
+;; Flags are mixed up with data that is not always inserted
+
+cell get_state_init_field(tuple state_init, int idx) inline {
+    cell res = null();
+    ;; Next flag index
+    int next_idx = 0;
+
+    do {
+        int flag = state_init.at(next_idx);
+
+        next_idx = flag ? next_idx + 2 : next_idx + 1;
+        idx -= 1;
+
+        if(idx == 0 & flag) {
+            res = state_init.at(next_idx - 1);
+        }
+
+    } until (~ res.null?() | idx <= 0)
+
+    return res;
+}
+
+_ validate_TIP_64(slice content_data) impure inline {
+
+    int content_layout = content_data~load_uint(8);
+
+    ;; Check for allowed content_layout
+    throw_unless(305, (content_layout == 1) | (content_layout == 0));
+
+    if(content_layout == 1) {
+
+        ;; Check that off-chain URI contains at least one ASCII char
+        throw_unless(306, token_snake_len(content_data) > 8);
+    } else {
+        ;; On-chain is stored as dict
+        ;; Has to be non-empty
+        throw_unless(306, content_data.preload_dict().dict_empty?());
+
+        ;; Perhaps could go further and test for Optional dict keys but none of those are required so i'll leave it be
+        ;; For now
+    }
+}
+
+int __test_change_owner() {
+
+	{-
+	  This method is not defined in TIP-62.
+	  Still this is de-facto standard.
+	  One could argue that this one should
+	  be moved elsewhere.
+	-}
+
+	var (owner_address, prev_item_index, content, nft_item_code, royalty_params) = load_test_data(); ;; this data will load from c4
+
+	int change_owner = 3; ;;Change owner op could be different
+	int query_id     = rand(1337) + 1;
+	slice rand_addr  = generate_internal_address_with_custom_data(0, 0, random());
+	;;Setting this random address as new collection owner.
+	builder msg_body = generate_internal_message_body( change_owner, query_id ).store_slice( rand_addr );
+	cell  msg        = generate_internal_message_custom( 0, 0, 0, msg_body, rand_addr, null(), 0 );
+
+	;; Should fail from non-owner address
+	int gas_failed   = invoke_method_expect_fail( recv_internal, [one_ton, one_ton, msg, msg_body.end_cell().begin_parse()] );
+
+	;;With correct owner it should succeed
+	msg              = generate_internal_message_custom( 0, 0, 0, msg_body, owner_address, null(), 0 );
+	var ( gas_success, _ ) = invoke_method( recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()] );
+
+	throw_unless( 500, equal_slices( rand_addr, get_owner() ) );
+
+
+	return gas_success + gas_failed;
+}
+
+int __test_deploy_item() {
+	{-
+	 Collection deployment behaviour is not really defined in TIP-62
+	 Besides get methods.
+	 Still have to test that it at least barely works right?
+	 Will test:
+
+	 1) Will test just that it won't allow deployment from non-owner
+	 2) Successfull deployment increases index and sends apropriate msgs
+	-}
+
+    var (owner_address, prev_item_index, content, nft_item_code, royalty_params) = load_test_data(); ;; this data will load from c4
+
+    slice rand_addr = generate_internal_address_with_custom_data(0, 0, random());
+
+	{-
+	  This is not the actual content format required for successfull deployment.
+	  Contrant will accept it and i think it's something to fix
+	  cell nft_content    = begin_cell().store_uint( 1, 8 ).store_slice("my_nft.json").end_cell();
+	-}
+
+    cell nft_content = begin_cell().store_slice("my_nft.json").end_cell();
+    ;; That's what actually should be sent for successfull deployment
+    cell nft_init = begin_cell().store_slice(owner_address).store_ref(nft_content).end_cell();
+    int query_id = rand(1337) + 1;
+    int forward_amount = one_ton / 10;
+    builder msg_body = generate_nft_deploy_request(0, nft_init, query_id, forward_amount);
+    cell msg = generate_internal_message_custom(0, 0, 0, msg_body, rand_addr, null(), 0);
+    ;; Should not allow deploy from non-owner address
+    int gas_used = invoke_method_expect_fail(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]);
+    assert_no_actions();
+    ;; Verify that non-owner address was error trigger
+    ;; Expect success
+
+    cell msg = generate_internal_message_custom(0, 0, 0, msg_body, owner_address, null(), 0);
+    (int gas_success, _) = invoke_method(recv_internal, [one_ton, 0, msg, msg_body.end_cell().begin_parse()]);
+
+    tuple actions = parse_c5();
+
+    throw_unless(500, actions.tuple_length() == 1);
+
+    (int action_type, cell body, int mode) = actions.at(0).untriple();
+
+    throw_unless(501, action_type == 0);
+
+    tuple parsed_msg = unsafe_tuple(parse_internal_message(body));
+
+    throw_unless(502, forward_amount == parsed_msg.at(5));
+
+    tuple state_init = parsed_msg.at(7);
+
+    ;; Deployment message has to have state_init
+    int state_len = state_init.tuple_length();
+
+    throw_unless(503, state_len > 0);
+
+    ;; Fourth flag is data segment of state_init
+
+    cell init_data = state_init.get_state_init_field(4);
+
+    ;; Data segment has to be present in ntf StateInit
+
+    throw_if(504, init_data.null?());
+
+    ;; Content in message body should be equal to what we sent
+
+    throw_unless(505, equal_slices(nft_init.begin_parse(), parsed_msg.at(8)));
+
+    ;; Nft index has to increase
+
+    throw_unless(506, get_nft_index() > prev_item_index);
+
+    return gas_used + gas_success;
+}
+
+int __test_royalty_msg () {
+
+	( int numirator, int denominator, slice dst ) = get_test_royalty(); ;;Could be load_test_royalty
+
+	int query_id        = rand( 1337 ) + 1;
+	slice src_addr	    = generate_internal_address_with_custom_data( 0, 0, random() );
+	builder msg_body    = generate_get_royalty_params( query_id );
+	cell msg	    = generate_internal_message_custom( 0, 0, 0, msg_body, src_addr, null(), 0 );
+	( int gas_used, _ ) = invoke_method( recv_internal, [ one_ton, one_ton, msg, msg_body.end_cell().begin_parse() ] );
+	tuple actions       = parse_c5();
+
+	throw_unless( 500, actions.tuple_length() == 1 );
+
+	( int action_type, cell body, int mode ) = actions.first().untriple();
+	throw_unless( 501, action_type == 0 );
+	throw_unless( 502, mode == 64 );
+
+
+	tuple parsed_msg    = unsafe_tuple( parse_internal_message(body) );
+
+	;;Message is sent back
+	throw_unless( 503, equal_slices( src_addr, parsed_msg.at( 4 ) ) );
+
+	slice msg_body      = parsed_msg.at( 8 );
+	throw_unless( 504, op_report_royalty_params == msg_body~load_uint( 32 ) );
+	throw_unless( 505, query_id == msg_body~load_uint( 64 ) );
+	throw_unless( 506, numirator == msg_body~load_uint( 16 ) );
+	throw_unless( 507, denominator == msg_body~load_uint( 16 ) );
+	throw_unless( 508, equal_slices( dst, msg_body~load_msg_addr() ) );
+
+
+	return gas_used;
+}
+
+;; Get methods test cases
+
+int __test_get_collection_data() {
+
+    var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_test_data();
+
+    var (gas_used, stack) = invoke_method(get_collection_data, []);
+    ;; Should return 3 values
+
+    throw_unless(600, stack.tuple_length() == 3);
+
+    ;; Index, content and owner address should equal to c4
+
+    int res_idx = stack.first();
+    throw_unless(601, next_item_index == res_idx);
+
+    ;; First ref of a content is returned
+    ;; Collection content
+
+    cell  res_content  = stack.second();
+    slice cont_slice   = content.begin_parse();
+    cell  coll_content = cont_slice~load_ref();
+
+    throw_unless(602, equal_slices(coll_content.begin_parse(), res_content.begin_parse()));
+
+    slice res_owner    = stack.third();
+    throw_unless(603, equal_slices(owner_address, res_owner));
+
+    ;; Let's check that CollectionContent is compliant to TIP-64
+
+    validate_TIP_64(coll_content.begin_parse());
+
+
+    return gas_used;
+}
+
+
+int __test_get_nft_content() {
+
+	;;Is there anything to be integrationally tested here?
+	;;Probably not
+
+    var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_test_data();
+    slice coll_slice = content.begin_parse();
+    cell nft_content = begin_cell().store_slice("my_nft.json").end_cell();
+
+    (_, cell collection_comm) = (coll_slice~load_ref(), coll_slice~load_ref());
+    cell concat_content = snake_concat_tagged(1, collection_comm, nft_content);
+
+    var (gas_used, stack) = invoke_method(get_nft_content, [0, nft_content]);
+
+    cell  res = stack.at(0);
+    slice res_slice = res.begin_parse();
+
+    throw_unless(601, snake_equal?(concat_content.begin_parse(), res_slice));
+
+    ;; Has to comply with TIP-64
+    validate_TIP_64(res_slice);
+
+    return gas_used;
+
+}
+
+{-
+  get_nft_address_by_index requires deployed nft item.
+  Thus it has been moved to integrational tests.
+-}
+
+int __test_royalty_params() {
+
+        var ( gas_used, stack ) = invoke_method( royalty_params, [] );
+
+        throw_unless( 700, stack.tuple_length() == 3 );
+        throw_unless( 701, is_int( stack.first() ) );
+        throw_unless( 702, is_int( stack.second() ) );
+        throw_unless( 703, is_slice( stack.third() ) );
+
+        parse_std_addr( stack.third() );
+
+        return gas_used;
+}
+
diff --git a/src/toncli/projects/nft_collection/tests/nft-tests-int.func b/src/toncli/projects/nft_collection/tests/nft-tests-int.func
new file mode 100644
index 0000000..28684e0
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/nft-tests-int.func
@@ -0,0 +1,24 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+int __test_get_nft_data_transfer() {
+	;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content)
+
+	var ( _ , new_owner, resp_dst ) = setup_transfer_addresses( false );
+	var ( gas_before, stack )       = invoke_method( get_nft_data, [] );
+
+	slice owner      = stack.fourth();
+	int query_id     = rand( 1337 ) + 1;
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), 0, null(), 0);
+	cell msg 	     = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ one_ton, one_ton, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_nft_data, [] );
+
+	throw_unless( 500, equal_slices( new_owner, stack.fourth() ) );
+
+
+	return gas_before + gas_transfer + gas_after;
+}
+
diff --git a/src/toncli/projects/nft_collection/tests/nft-tests.func b/src/toncli/projects/nft_collection/tests/nft-tests.func
new file mode 100644
index 0000000..52f6945
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/nft-tests.func
@@ -0,0 +1,583 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+slice get_owner_addr() inline {
+	slice ds = get_data().begin_parse().skip_bits( 64 );
+	ds~load_msg_addr();
+	return ds~load_msg_addr();
+}
+
+slice gen_new_owner( slice old_owner ) inline {
+	slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	;; In theory we can win a bingo and get same 256b integer from RNG
+	while( equal_slices( new_owner, old_owner ) ){
+		new_owner = generate_internal_address();
+	}
+
+	return new_owner;
+}
+
+( int, slice, slice, cell ) load_test_data() inline {
+	slice ds	= get_data().begin_parse();
+	int idx		= ds~load_uint( 64 );
+	slice coll_addr = ds~load_msg_addr();
+	slice owner	= ds~load_msg_addr();
+
+	;;We need all addresses on Workchain 0 otherwise force_chain will throw 333
+
+	if (coll_addr.preload_uint(2) != 0){ ;; if not addr_none
+		coll_addr.force_chain();
+	}
+
+	if (owner.preload_uint(2) != 0){
+		owner.force_chain();
+	}
+
+	return ( idx, coll_addr, owner, ds~load_ref() );
+}
+
+( slice, slice, slice ) setup_transfer_addresses( int has_dst? ) inline {
+	slice owner_addr = get_owner_addr();
+
+	;;We need all addresses on Workchain 0 otherwise force_chain will throw 333
+	
+	slice new_owner   = gen_new_owner( owner_addr );
+
+	slice resp_dst    = has_dst? ? generate_internal_address_with_custom_data( 0, 0, random() ) : generate_empty_address();
+
+	return ( owner_addr, new_owner, resp_dst );
+
+}
+
+_ verify_ownership_assigned( int query_id, slice owner, slice new_owner, int forward_amount, slice forward_payload, cell msg ) impure inline {
+	{-
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+	We verify that:
+	1) message is sent to the new_owner address
+	2) attached amount matches forward_amount
+	3) op matches ownership_assigned op
+	4) query_id matches query_id sent
+	5) forward_payload matches payload set
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	throw_unless(601, equal_slices( new_owner, parsed_msg.at( 4 ) ) ); 
+	;; Check that forward_amount matches
+	throw_unless(602, forward_amount == parsed_msg.at( 5 ) );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 603, op_ownership_assigned == msg_body~load_uint( 32 ) );
+
+	throw_unless( 604, query_id == msg_body~load_uint( 64 ) );
+	
+	;;Checking for previous owner addr
+	throw_unless( 605, equal_slices( owner, msg_body~load_msg_addr() ) );
+
+	if( ~ null?( forward_payload ) ){
+		if( msg_body~load_uint( 1 ) ){
+			cell msg_payload = msg_body~load_ref();
+			throw_unless( 606, equal_slices( forward_payload, msg_payload.begin_parse() ) );
+		}
+		else {
+			throw_unless( 606, equal_slices( forward_payload, msg_body  ) );
+		}
+	}
+}
+
+_ verify_excess_sent( int query_id, slice resp_dst, int balance, int forward_fee, int forward_amount, cell msg ) impure inline {
+	{-
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+	Excess message should be sent to resp_dst with all of the msg_value - fees taken to process
+	We verify that:
+	1) message is sent to resp_dst
+	2) attached amount is balance - fees taken - forward_amount
+	3) op matches excess op
+	4) query_id matches request query_id
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;;Check dst_addr to be equal to resp_dst
+	throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) );
+
+	int total_sent  = parsed_msg.at( 5 );
+	int should_sent = balance - min_storage - forward_fee;
+
+	if( forward_amount > 0 ) {
+		should_sent -= forward_amount + forward_fee;
+	}
+
+	throw_unless( 702, should_sent == total_sent );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) );
+
+	throw_unless( 704, query_id == msg_body~load_uint( 64 ) );
+
+}
+
+;;Transfer ownership reject cases
+
+int __test_transfer_not_owner(){
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	;;Trying to execute transfer ownership from non-owner addr
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton / 10, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, new_owner, null(), 0 );
+
+	;;Should fail
+	int gas_used 	  = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	{-
+	 These are for testing this specific contract exit_code values invoke_method_expect_fail is more general way
+	 var ( exit_code, gas_used, _ ) = invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+	 throw_unless( exit_code, exit_code == 401 );
+	 Should not generate any actions
+	-}
+
+	assert_no_actions();
+
+	;;Now let's verify that owner_addr triggered fail by changin source addr to actual owner_addr
+	;;Expect contract to return success
+
+	msg 	  	  	= generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	( int gas_success , _ ) = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_used + gas_success;
+
+}
+
+int __test_transfer_forward_amount_too_large() {
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 NFT should reject transfer if balance lower than forward_amount + message forward fee + minimal storage fee
+	 Sending message with forward_amount of 1 TON and balance 0.1 TON
+	 Now using legit owner address
+	 TON balance:0.1 forward_amount:1 TON fwd_fee:0 verifying that forward_amount is taken into account
+	 Should fail with no actions
+	-}
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+	int gas_used 	  = invoke_method_expect_fail( recv_internal, [ one_ton / 10, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;;(int exit_code, _, _ ) =  invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+	;;throw_unless( exit_code, exit_code == 402 );
+
+	assert_no_actions();
+
+	;;Now verify that balance was the error trigger by increasing contract balance to 10 TONs
+	;;Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used + gas_success;
+}
+
+int __test_transfer_storage_fee() {
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 Now let's try forward_amount exactly equal to balance and fwd_fee 0
+	 1 TON Balance forward_amount:1 TON fwd_fee:0 verifying that minimal storage comes into play
+	 Should fail with no actions
+	-}
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	assert_no_actions();
+
+	;; Let's verify that storage fee was an error trigger by increasing balance by min_storage 
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton + min_storage, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used + gas_success;
+}
+
+int __test_transfer_forward_fee_single() {
+	{-
+	 If transfer is successfull NFT supposed to send up to 2 messages
+	 1)To the owner_address with forward_amount of coins
+	 2)To the response_addr with forward_payload if response_addr is not addr_none
+	 Each of those messages costs fwd_fee
+	 Let' test the first case only by setting resp_dst to addr_none
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	{-
+	 Now we test if contract takes forward fee into account by adding forward fee to the incoming message
+	 Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON
+	 Should fail with no actions
+	-}
+
+	int forward_fee	  = one_ton / 100;
+	int balance	  = one_ton + min_storage;
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee);
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Let's verify that forward fee was an error trigger by increasing balance by that fee
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ balance + forward_fee, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used; 
+}
+
+int __test_transfer_forward_fee_double() {
+
+	{-
+	 If transfer is successfull NFT supposed to send up to 2 messages
+	 1)To the owner_address with forward_amount of coins
+	 2)To the response_addr with forward_payload if response_addr is not addr_none
+	 Each of those messages costs fwd_fee
+	 In this case we test scenario where both messages required to be sent but balance has funs only for single message
+	 To do so resp_dst has be a valid address not equal to addr_none
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 Now we test if contract takes forward fee into account by adding forward fee to the incoming message
+	 Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON
+	 Should fail with no actions
+	-}
+
+	int forward_fee	  = one_ton / 100;
+	int balance	  = one_ton + min_storage + forward_fee;
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee);
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Let's verify that double forward fee was an error trigger by increasing balance by another forward_fee
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ balance + forward_fee, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used; 
+}
+
+int __test_success_no_forward_no_reponse() {
+
+	{-
+	 forward_amount:0 resp_dst:addr_none
+	 On successfull execution only address change should occur.
+	 Expect no messages to be sent.
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), 0, null(), 0 );
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	(int gas_used, _ )  = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	;; resp_dst is addr_none and no forward_amount, so no message should be generated
+	assert_no_actions();
+
+	return gas_used;
+
+}
+
+;; Transfer ownership success cases
+
+int __test_transfer_success_forward_no_response() {
+
+	{-
+	forward_amount:1TON resp_dst: addr_none balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+
+	forward_payload should be equal with request's forward_payload.
+
+	prev_owner is address of the previous owner of this NFT item.
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= one_ton;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1337 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Only one message should be sent
+	throw_unless( 500 + actions_count, actions_count == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body  );
+
+
+	return gas_used;
+}
+
+int __test_transfer_success_forward_with_response() {
+
+	{-
+	forward_amount:1TON resp_dst: addr_none balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+
+	forward_payload should be equal with request's forward_payload.
+
+	prev_owner is address of the previous owner of this NFT item.
+
+	3) Single excesses message to resp_dst
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+
+	For second message to be sent resp_dst should be valid address not equal to addr_none
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= one_ton;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1338 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	;;Attach 5 TONs to message value to test excess
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Two messages should be sent
+	throw_unless( 500 + actions_count, actions_count == 2 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body  );
+
+	( action_type, body, mode ) = actions.at( 1 ).untriple();
+
+	throw_unless( 700, action_type == 0);
+
+	verify_excess_sent( query_id, resp_dst, balance, forward_fee, forward_amount, body );
+
+	return gas_used;
+}
+
+
+int __test_transfer_success_response_only() {
+
+	{-
+	forward_amount:0 TON resp_dst: valid address balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single excesses message to resp_dst
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+
+	For second message to be sent resp_dst should be valid address not equal to addr_none
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= 0;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1337 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Only one message should be sent
+	throw_unless( 500 + actions_count, actions_count == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_excess_sent( query_id, resp_dst, balance, forward_fee, forward_amount, body );
+
+	return gas_used;
+}
+
+;;get_static_data test cases
+
+int __test_get_static_data() {
+	{-
+	Sends get_static_data request.
+	Expect success.
+	On success execution expect:
+	1) Single report_static_data message received
+	2) Message send mode is 64
+	3) query_id matches query_id in request
+	4) Message has 256 bit index value
+	5) Message has valid collection_address matching c4 collection_addr
+	-}
+
+	int query_id = rand( 1337 ) + 1;
+
+	var ( idx, coll_addr, owner, _ ) = load_test_data();
+	builder msg_body = generate_nft_get_static_data_request( query_id );
+	cell msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	( int gas_used, _ ) = invoke_method( recv_internal,[ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] ); 
+
+	tuple actions	 = parse_c5();
+
+	throw_unless( 800, actions.tuple_length() == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	throw_unless( 801, action_type == 0 );
+
+	throw_unless( 802, mode == 64 );
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( body ) );
+	slice ds 	 = parsed_msg.at( 8 );
+
+	( int op, int resp_query_id ) = ( ds~load_uint( 32 ), ds~load_uint( 64 ) );
+
+	throw_unless( 803, op_report_static_data == op );
+
+	throw_unless( 804, query_id == resp_query_id );
+
+	throw_unless( 805, idx == ds~load_uint( 256 ) );
+
+	throw_unless( 806, equal_slices( coll_addr, ds~load_msg_addr() ) );
+
+	return gas_used;
+}
+
+;; Get methods test
+
+int __test_get_nft_data() {
+	;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content)
+
+	var ( idx, coll_addr, owner, content ) = load_test_data();
+
+	( int gas_used, tuple stack ) = invoke_method( get_nft_data, [] );
+
+	;;Check that argument count match signature
+
+	throw_unless( 300, stack.tuple_length() == 5 );
+	int init? = stack.first();
+	int res_idx = stack.second();
+	slice res_collection = stack.third();
+	slice res_owner      = stack.fourth();
+	cell  res_content    = stack.at( 4 );
+
+	;; Check that initialized NFT can't have empty owner_address
+	;; And the other way around
+	throw_unless( 301, ( init? ^ res_owner.null?() ) );
+
+	;; Check that contract correctly loads data
+	throw_unless( 302, equal_slices( owner, res_owner ) );
+	throw_unless( 303, equal_slices( coll_addr, res_collection ) );
+
+	slice content_data = res_content.begin_parse();
+
+	throw_unless( 304, equal_slices( content.begin_parse(), content_data ) );
+
+	;; Only collection-less NFT have to comply with TIP-64
+	;; Such NFT items have addr_none as collection_address
+	if( equal_slices( res_owner, generate_empty_address() ) ){
+
+		int content_layout = content_data~load_uint( 8 );
+
+		;; Check for allowed content_layout
+		throw_unless( 305, ( content_layout  == 1 ) | ( content_layout == 0 ) );
+
+
+		if( content_layout == 1 ){
+
+			;; Check that off-chain URI contains at least one ASCII char
+			throw_unless( 306, token_snake_len( content_data ) > 8 );
+		} else {
+			;; On-chain is stored as dict
+			;; Has to be non-empty
+			throw_unless( 306, content_data.preload_dict().dict_empty?() );
+
+			;; Perhaps could go further and test for Optional dict keys but none of those is required so i'll leave it be
+		}
+	}
+
+
+	return gas_used;
+}
diff --git a/src/toncli/projects/nft_collection/tests/utils/collection-data.func b/src/toncli/projects/nft_collection/tests/utils/collection-data.func
new file mode 100644
index 0000000..e1bdf33
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/utils/collection-data.func
@@ -0,0 +1,38 @@
+(slice, int, cell, cell, cell) load_test_data() inline {
+    var ds = get_data().begin_parse();
+    return (    ds~load_msg_addr(), ;; owner_address
+                ds~load_uint(64), ;; next_item_index
+                ds~load_ref(), ;; content
+                ds~load_ref(), ;; nft_item_code
+                ds~load_ref() ;; royalty_params
+            );
+}
+
+slice get_owner() {
+    (slice owner, _, _, _, _) = load_test_data();
+    return owner;
+}
+
+cell get_content() inline {
+    (_, _, cell content, _, _) = load_test_data();
+    return content;
+}
+
+int get_nft_index() inline {
+    (_, int idx, _, _, _) = load_test_data();
+
+    return idx;
+}
+
+( int, int, slice ) load_test_royalty() {
+	( _, _, _, _, cell royalty ) = load_test_data();
+	slice ds = royalty.begin_parse();
+
+	return ( ds~load_uint( 16 ), ds~load_uint( 16 ), ds~load_msg_addr() );
+}
+
+( int, int, slice ) get_test_royalty() {
+	( _, tuple stack ) = invoke_method( royalty_params, [] );
+
+	return ( stack.first(), stack.second(), stack.third() );
+}
diff --git a/src/toncli/projects/nft_collection/tests/utils/constants.func b/src/toncli/projects/nft_collection/tests/utils/constants.func
new file mode 100644
index 0000000..42f8c31
--- /dev/null
+++ b/src/toncli/projects/nft_collection/tests/utils/constants.func
@@ -0,0 +1,2 @@
+const int one_ton = 1000000000; ;;10^9
+const int min_storage = 50000000; ;; 0.05 TON minimal storage fee
diff --git a/src/toncli/projects/nft_item/tests/.gitkeep b/src/toncli/projects/nft_item/tests/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/toncli/projects/nft_item/tests/nft-tests-int.func b/src/toncli/projects/nft_item/tests/nft-tests-int.func
new file mode 100644
index 0000000..28684e0
--- /dev/null
+++ b/src/toncli/projects/nft_item/tests/nft-tests-int.func
@@ -0,0 +1,24 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+int __test_get_nft_data_transfer() {
+	;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content)
+
+	var ( _ , new_owner, resp_dst ) = setup_transfer_addresses( false );
+	var ( gas_before, stack )       = invoke_method( get_nft_data, [] );
+
+	slice owner      = stack.fourth();
+	int query_id     = rand( 1337 ) + 1;
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), 0, null(), 0);
+	cell msg 	     = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	var ( gas_transfer, _ )  = invoke_method( recv_internal, [ one_ton, one_ton, msg, msg_body.end_cell().begin_parse() ] );
+	var ( gas_after, stack ) = invoke_method( get_nft_data, [] );
+
+	throw_unless( 500, equal_slices( new_owner, stack.fourth() ) );
+
+
+	return gas_before + gas_transfer + gas_after;
+}
+
diff --git a/src/toncli/projects/nft_item/tests/nft-tests.func b/src/toncli/projects/nft_item/tests/nft-tests.func
new file mode 100644
index 0000000..8b6dd43
--- /dev/null
+++ b/src/toncli/projects/nft_item/tests/nft-tests.func
@@ -0,0 +1,580 @@
+#pragma version >=0.2.0;
+
+#include "utils/constants.func";
+
+slice get_owner_addr() inline {
+	slice ds = get_data().begin_parse().skip_bits( 64 );
+	ds~load_msg_addr();
+	return ds~load_msg_addr();
+}
+
+slice gen_new_owner( slice old_owner ) inline {
+	slice new_owner = generate_internal_address_with_custom_data( 0, 0, random() );
+
+	;; In theory we can win a bingo and get same 256b integer from RNG
+	while( equal_slices( new_owner, old_owner ) ){
+		new_owner = generate_internal_address();
+	}
+
+	return new_owner;
+}
+
+( int, slice, slice, cell ) load_test_data() inline {
+	slice ds	= get_data().begin_parse();
+	int idx		= ds~load_uint( 64 );
+	slice coll_addr = ds~load_msg_addr(); ;; if single nft - addr_none here
+	slice owner	= ds~load_msg_addr();
+
+	;;We need all addresses on Workchain 0 otherwise force_chain will throw 333
+	if (coll_addr.preload_uint(2) != 0){ ;; if not addr_none
+		coll_addr.force_chain();
+	}
+
+	if (owner.preload_uint(2) != 0){
+		owner.force_chain();
+	}
+
+	return ( idx, coll_addr, owner, ds~load_ref() );
+}
+
+( slice, slice, slice ) setup_transfer_addresses( int has_dst? ) inline {
+	slice owner_addr = get_owner_addr();
+
+	;;We need all addresses on Workchain 0 otherwise force_chain will throw 333
+	
+	slice new_owner   = gen_new_owner( owner_addr );
+
+	slice resp_dst    = has_dst? ? generate_internal_address_with_custom_data( 0, 0, random() ) : generate_empty_address();
+
+	return ( owner_addr, new_owner, resp_dst );
+
+}
+
+_ verify_ownership_assigned( int query_id, slice owner, slice new_owner, int forward_amount, slice forward_payload, cell msg ) impure inline {
+	{-
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+	We verify that:
+	1) message is sent to the new_owner address
+	2) attached amount matches forward_amount
+	3) op matches ownership_assigned op
+	4) query_id matches query_id sent
+	5) forward_payload matches payload set
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	throw_unless(601, equal_slices( new_owner, parsed_msg.at( 4 ) ) ); 
+	;; Check that forward_amount matches
+	throw_unless(602, forward_amount == parsed_msg.at( 5 ) );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 603, op_ownership_assigned == msg_body~load_uint( 32 ) );
+
+	throw_unless( 604, query_id == msg_body~load_uint( 64 ) );
+	
+	;;Checking for previous owner addr
+	throw_unless( 605, equal_slices( owner, msg_body~load_msg_addr() ) );
+
+	if( ~ null?( forward_payload ) ){
+		if( msg_body~load_uint( 1 ) ){
+			cell msg_payload = msg_body~load_ref();
+			throw_unless( 606, equal_slices( forward_payload, msg_payload.begin_parse() ) );
+		}
+		else {
+			throw_unless( 606, equal_slices( forward_payload, msg_body  ) );
+		}
+	}
+}
+
+_ verify_excess_sent( int query_id, slice resp_dst, int balance, int forward_fee, int forward_amount, cell msg ) impure inline {
+	{-
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+	Excess message should be sent to resp_dst with all of the msg_value - fees taken to process
+	We verify that:
+	1) message is sent to resp_dst
+	2) attached amount is balance - fees taken - forward_amount
+	3) op matches excess op
+	4) query_id matches request query_id
+	-}
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( msg ) );
+
+	;;Check dst_addr to be equal to resp_dst
+	throw_unless( 701, equal_slices( resp_dst, parsed_msg.at( 4 ) ) );
+
+	int total_sent  = parsed_msg.at( 5 );
+	int should_sent = balance - min_storage - forward_fee;
+
+	if( forward_amount > 0 ) {
+		should_sent -= forward_amount + forward_fee;
+	}
+
+	throw_unless( 702, should_sent == total_sent );
+
+	slice msg_body = parsed_msg.at( 8 );
+
+	throw_unless( 703, op_excesses == msg_body~load_uint( 32 ) );
+
+	throw_unless( 704, query_id == msg_body~load_uint( 64 ) );
+
+}
+
+;;Transfer ownership reject cases
+
+int __test_transfer_not_owner(){
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	;;Trying to execute transfer ownership from non-owner addr
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton / 10, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, new_owner, null(), 0 );
+
+	;;Should fail
+	int gas_used 	  = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	{-
+	 These are for testing this specific contract exit_code values invoke_method_expect_fail is more general way
+	 var ( exit_code, gas_used, _ ) = invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+	 throw_unless( exit_code, exit_code == 401 );
+	 Should not generate any actions
+	-}
+
+	assert_no_actions();
+
+	;;Now let's verify that owner_addr triggered fail by changin source addr to actual owner_addr
+	;;Expect contract to return success
+
+	msg 	  	  	= generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	( int gas_success , _ ) = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	return gas_used + gas_success;
+
+}
+
+int __test_transfer_forward_amount_too_large() {
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 NFT should reject transfer if balance lower than forward_amount + message forward fee + minimal storage fee
+	 Sending message with forward_amount of 1 TON and balance 0.1 TON
+	 Now using legit owner address
+	 TON balance:0.1 forward_amount:1 TON fwd_fee:0 verifying that forward_amount is taken into account
+	 Should fail with no actions
+	-}
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+	int gas_used 	  = invoke_method_expect_fail( recv_internal, [ one_ton / 10, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;;(int exit_code, _, _ ) =  invoke_method_full( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+	;;throw_unless( exit_code, exit_code == 402 );
+
+	assert_no_actions();
+
+	;;Now verify that balance was the error trigger by increasing contract balance to 10 TONs
+	;;Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used + gas_success;
+}
+
+int __test_transfer_storage_fee() {
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 Now let's try forward_amount exactly equal to balance and fwd_fee 0
+	 1 TON Balance forward_amount:1 TON fwd_fee:0 verifying that minimal storage comes into play
+	 Should fail with no actions
+	-}
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	assert_no_actions();
+
+	;; Let's verify that storage fee was an error trigger by increasing balance by min_storage 
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ one_ton + min_storage, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used + gas_success;
+}
+
+int __test_transfer_forward_fee_single() {
+	{-
+	 If transfer is successfull NFT supposed to send up to 2 messages
+	 1)To the owner_address with forward_amount of coins
+	 2)To the response_addr with forward_payload if response_addr is not addr_none
+	 Each of those messages costs fwd_fee
+	 Let' test the first case only by setting resp_dst to addr_none
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	{-
+	 Now we test if contract takes forward fee into account by adding forward fee to the incoming message
+	 Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON
+	 Should fail with no actions
+	-}
+
+	int forward_fee	  = one_ton / 100;
+	int balance	  = one_ton + min_storage;
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee);
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Let's verify that forward fee was an error trigger by increasing balance by that fee
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ balance + forward_fee, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used; 
+}
+
+int __test_transfer_forward_fee_double() {
+
+	{-
+	 If transfer is successfull NFT supposed to send up to 2 messages
+	 1)To the owner_address with forward_amount of coins
+	 2)To the response_addr with forward_payload if response_addr is not addr_none
+	 Each of those messages costs fwd_fee
+	 In this case we test scenario where both messages required to be sent but balance has funs only for single message
+	 To do so resp_dst has be a valid address not equal to addr_none
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	{-
+	 Now we test if contract takes forward fee into account by adding forward fee to the incoming message
+	 Contract balance would be 1TON + storage_fee and fwd_fee would be 0.01 TON
+	 Should fail with no actions
+	-}
+
+	int forward_fee	  = one_ton / 100;
+	int balance	  = one_ton + min_storage + forward_fee;
+
+	builder msg_body  = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), one_ton, null(), 0 );
+
+	cell msg 	  = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee);
+
+	int gas_used	  = invoke_method_expect_fail( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+	assert_no_actions();
+
+	;; Let's verify that double forward fee was an error trigger by increasing balance by another forward_fee
+	;; Expect success
+
+	( int gas_success, _ ) = invoke_method( recv_internal, [ balance + forward_fee, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+
+	return gas_used; 
+}
+
+int __test_success_no_forward_no_reponse() {
+
+	{-
+	 forward_amount:0 resp_dst:addr_none
+	 On successfull execution only address change should occur.
+	 Expect no messages to be sent.
+	-}
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, 12345, null(), 0, null(), 0 );
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), 0 );
+
+	(int gas_used, _ )  = invoke_method( recv_internal, [ one_ton, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	;; resp_dst is addr_none and no forward_amount, so no message should be generated
+	assert_no_actions();
+
+	return gas_used;
+
+}
+
+;; Transfer ownership success cases
+
+int __test_transfer_success_forward_no_response() {
+
+	{-
+	forward_amount:1TON resp_dst: addr_none balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+
+	forward_payload should be equal with request's forward_payload.
+
+	prev_owner is address of the previous owner of this NFT item.
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( false );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= one_ton;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1337 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Only one message should be sent
+	throw_unless( 500 + actions_count, actions_count == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body  );
+
+
+	return gas_used;
+}
+
+int __test_transfer_success_forward_with_response() {
+
+	{-
+	forward_amount:1TON resp_dst: addr_none balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single ownership_assigned message is sent to new_owner addr with forward_amount attached
+	TL-B schema: ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress
+	forward_payload:(Either Cell ^Cell) = InternalMsgBody;
+	query_id should be equal with request's query_id.
+
+	forward_payload should be equal with request's forward_payload.
+
+	prev_owner is address of the previous owner of this NFT item.
+
+	3) Single excesses message to resp_dst
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+
+	For second message to be sent resp_dst should be valid address not equal to addr_none
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= one_ton;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1338 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	;;Attach 5 TONs to message value to test excess
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Two messages should be sent
+	throw_unless( 500 + actions_count, actions_count == 2 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_ownership_assigned( query_id, owner_addr, new_owner, forward_amount, forward_payload.end_cell().begin_parse(), body  );
+
+	( action_type, body, mode ) = actions.at( 1 ).untriple();
+
+	throw_unless( 700, action_type == 0);
+
+	verify_excess_sent( query_id, resp_dst, balance, forward_fee, forward_amount, body );
+
+	return gas_used;
+}
+
+
+int __test_transfer_success_response_only() {
+
+	{-
+	forward_amount:0 TON resp_dst: valid address balance:10TON
+	forward_payload:"Hop hey!"
+	On successfull execution expect:
+	1) Address change to new owner
+	2) Single excesses message to resp_dst
+	TL-B schema: excesses#d53276db query_id:uint64 = InternalMsgBody;
+
+	For second message to be sent resp_dst should be valid address not equal to addr_none
+	-} 
+
+	var ( owner_addr, new_owner, resp_dst ) = setup_transfer_addresses( true );
+
+	int forward_fee         = one_ton / 100;
+	int forward_amount	= 0;
+	int balance 	        = one_ton * 10;
+	int query_id	        = rand( 1337 ) + 1;
+	builder forward_payload = begin_cell().store_slice("Hop hey!");
+
+
+	builder msg_body = generate_nft_transfer_request( new_owner, resp_dst, query_id,  null(), forward_amount, forward_payload.end_cell(), 0);
+	cell msg 	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner_addr, null(), forward_fee );
+
+	(int gas_used, _ )    = invoke_method( recv_internal, [ balance, 0, msg, msg_body.end_cell().begin_parse() ] );
+
+	;; Owner address has to be changed to the new_owner
+	throw_unless( 100, equal_slices( get_owner_addr(), new_owner ) );
+
+	tuple actions = parse_c5(); ;; test-libs/c5_parse_helpers.func
+
+	int actions_count = actions.tuple_length();
+
+	;; Only one message should be sent
+	throw_unless( 500 + actions_count, actions_count == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	;; Action has to be action_send_msg
+	throw_unless( 600, action_type == 0);
+
+	verify_excess_sent( query_id, resp_dst, balance, forward_fee, forward_amount, body );
+
+	return gas_used;
+}
+
+;;get_static_data test cases
+
+int __test_get_static_data() {
+	{-
+	Sends get_static_data request.
+	Expect success.
+	On success execution expect:
+	1) Single report_static_data message received
+	2) Message send mode is 64
+	3) query_id matches query_id in request
+	4) Message has 256 bit index value
+	5) Message has valid collection_address matching c4 collection_addr
+	-}
+
+	int query_id = rand( 1337 ) + 1;
+
+	var ( idx, coll_addr, owner, _ ) = load_test_data();
+	builder msg_body = generate_nft_get_static_data_request( query_id );
+	cell msg	 = generate_internal_message_custom( 0, 0, 0, msg_body, owner, null(), 0 );
+
+	( int gas_used, _ ) = invoke_method( recv_internal,[ one_ton * 10, 0, msg, msg_body.end_cell().begin_parse() ] ); 
+
+	tuple actions	 = parse_c5();
+
+	throw_unless( 800, actions.tuple_length() == 1 );
+
+	( int action_type, cell body, int mode ) = actions.at(0).untriple();
+
+	throw_unless( 801, action_type == 0 );
+
+	throw_unless( 802, mode == 64 );
+
+	tuple parsed_msg = unsafe_tuple( parse_internal_message( body ) );
+	slice ds 	 = parsed_msg.at( 8 );
+
+	( int op, int resp_query_id ) = ( ds~load_uint( 32 ), ds~load_uint( 64 ) );
+
+	throw_unless( 803, op_report_static_data == op );
+
+	throw_unless( 804, query_id == resp_query_id );
+
+	throw_unless( 805, idx == ds~load_uint( 256 ) );
+
+	throw_unless( 806, equal_slices( coll_addr, ds~load_msg_addr() ) );
+
+	return gas_used;
+}
+
+;; Get methods test
+
+int __test_get_nft_data() {
+	;; (int init?, int index, slice collection_address, slice owner_address, cell individual_content)
+	var ( idx, coll_addr, owner, content ) = load_test_data();
+
+	( int gas_used, tuple stack ) = invoke_method( get_nft_data, [] );
+
+	;;Check that argument count match signature
+	throw_unless( 300, stack.tuple_length() == 5 );
+	int init? = stack.first();
+	int res_idx = stack.second();
+	slice res_collection = stack.third();
+	slice res_owner      = stack.fourth();
+	cell  res_content    = stack.at( 4 );
+
+	;; Check that initialized NFT can't have empty owner_address
+	;; And the other way around
+	throw_unless( 301, ( init? ^ res_owner.null?() ) );
+
+	;; Check that contract correctly loads data
+	throw_unless( 302, equal_slices( owner, res_owner ) );
+	throw_unless( 303, equal_slices( coll_addr, res_collection ) );
+
+	slice content_data = res_content.begin_parse();
+
+	throw_unless( 304, equal_slices( content.begin_parse(), content_data ) );
+
+	;; Only collection-less NFT have to comply with TIP-64
+	;; Such NFT items have addr_none as collection_address
+	if( equal_slices( res_owner, generate_empty_address() ) ){
+
+		int content_layout = content_data~load_uint( 8 );
+
+		;; Check for allowed content_layout
+		throw_unless( 305, ( content_layout  == 1 ) | ( content_layout == 0 ) );
+
+
+		if( content_layout == 1 ){
+
+			;; Check that off-chain URI contains at least one ASCII char
+			throw_unless( 306, token_snake_len( content_data ) > 8 );
+		} else {
+			;; On-chain is stored as dict
+			;; Has to be non-empty
+			throw_unless( 306, content_data.preload_dict().dict_empty?() );
+
+			;; Perhaps could go further and test for Optional dict keys but none of those is required so i'll leave it be
+		}
+	}
+
+
+	return gas_used;
+}
diff --git a/src/toncli/projects/nft_item/tests/utils/constants.func b/src/toncli/projects/nft_item/tests/utils/constants.func
new file mode 100644
index 0000000..42f8c31
--- /dev/null
+++ b/src/toncli/projects/nft_item/tests/utils/constants.func
@@ -0,0 +1,2 @@
+const int one_ton = 1000000000; ;;10^9
+const int min_storage = 50000000; ;; 0.05 TON minimal storage fee