Debugging Tact contracts
Without fail, the code we write as smart contract developers doesn’t always do what we expected it to do. Sometimes it does something completely different! When the unexpected happens, the next task is to figure out why. To do so, there are various ways to reveal problems or “bugs” in the code. Let’s get to debugging!
General approach
At the moment, Tact doesn’t have a step-through debugger. Despite that, it’s still possible to use the “printf debugging” approach.
It involves actively placing dump()
and dumpStack()
function calls throughout your code and observing states of variables at a given point of time. Note, that those functions work only in a debug mode and won’t be executed otherwise.
In addition to dumping values, it’s often helpful to use assertive functions like require()
, nativeThrowIf()
and nativeThrowUnless()
. They help stating your assumptions clear, and are handy for setting up “trip wires” for catching issues in the future.
And if you didn’t find or cannot resolve the cause of your issues, try asking the community in Tact’s Telegram chat or, if your issue or question is generally related to TON more than it’s related to Tact, hop into TON Dev Telegram chat.
Common debugging functions
Tact provides a handful amount of various functions useful for debugging: Core library → Debug.
Enabling debug mode in compilation options
In order to make certain functions like dump()
or dumpStack()
work, one needs to enable debug mode.
The simplest and recommended approach is to modify a tact.config.json
file in the root of your project (or create it if it didn’t exist yet), and set the debug
property to true
.
If you’re working on a Blueprint-based project, you can enable debug mode in the compilation configs of your contracts, which are located in a directory named wrappers/
:
Note, that versions of Blueprint starting with 0.20.0 automatically enable debug mode in wrappers/
for new contracts.
In addition to that, tact.config.json
may still be used in Blueprint projects. In such cases values specified in tact.config.json
act as default unless modified in the wrappers/
.
Writing tests in Blueprint, with Sandbox and Jest
The Blueprint is a popular development framework for writing, testing and deploying smart contracts on TON Blockchain.
For testing smart contracts it uses the Sandbox, a local TON Blockchain emulator and Jest, a JavaScript testing framework.
Whenever you create a new Blueprint project or use blueprint create
command inside the existing project, it creates a new contract alongside with a test suite file for it.
Those files are placed in tests/
folder and executed with Jest. By default, all tests run, unless you specify specific group or test closure. For other options, refer to the brief documentation in the Jest CLI: jest --help
.
Structure of test files
Let’s say that we have a contract named Playground
, written in contracts/playground.tact
file. If we’ve created that contract through Blueprint, then it also created a tests/Playground.spec.ts
test suite file for us.
The test file contains a single describe()
Jest function call, which denotes a test group.
Inside that group, you’ll have three variables, available in all tests within:
blockchain
— local blockchain instance provided by Sandboxdeployer
— a TypeScript wrapper used for deploying ourPlayground
contract or any other we’d like to be deployedplayground
— a TypeScript wrapper for ourPlayground
contract
Then, a beforeEach()
Jest function is called — it specifies all the code to be executed before each of the subsequent test closures.
Finally, each test closure is described with a call to it()
Jest function — that’s where tests are actually written.
A simplest example of the test closure can look like that:
Debug with dump()
To see results of dump()
function calls and use “printf debugging” approach, one has to:
- Put calls to
dump()
and other common debugging functions in relevant places of the code. - Run Jest tests, which would call target functions and send messages to target receivers.
Assuming you’ve created a new counter contract project, let’s see how it works in practice.
First, let’s place a call to dump()
in contracts/simple_counter.tact
, which would output the amount
passed in msg
Struct to contract’s debug console:
Next, let’s comment out all existing it()
test closures in tests/SimpleCounter.spec.ts
file. And then add the following one:
It sends a message to our contract’s receive(msg: Add)
receiver without storing the results of such send.
Now, if we build our contract with yarn build
and run our test suite with yarn test
, we’ll see the following in the test logs:
Which is produced by of our dump()
call above.
State expectations with expect()
The integral parts of writing tests is ensuring that your expectations match the observed reality. For that, Jest provides a function expect()
, which is used as follows:
- First, an observed variable is provided.
- Then, a specific method is called to check a certain property of that variable.
Here’s a more involved example, which uses expect()
function to check that counter contract actually properly increases the counter:
Utility methods
Test files generated by Blueprint import @ton/test-utils
library, which provides access to a number of additional helper methods for the result type of expect()
Jest function. Note, that regular methods like toEqual()
are still there and ready to be used.
toHaveTransaction
The method expect(…).toHaveTransaction()
checks that the list of transactions has a transaction matching certain properties you specify:
To know the full list of such properties, look at auto-completion options provided by your editor or IDE.
toEqualCell
The method expect(…).toEqualCell()
checks equality of two cells:
toEqualSlice
The method expect(…).toEqualSlice()
checks equality of two slices:
toEqualAddress
The method expect(…).toEqualAddress()
checks equality of two addresses:
Send messages to contracts
To send messages to contracts, use .send()
method on their TypeScript wrappers like so:
Message body can be a simple string, or an object specifying fields of the Message type:
More often than not, it’s important to store results of such sends, because they contain events occurred, transactions made and external messages sent:
With that, we can easily filter or check certain transactions:
Observe the fees and values
Sandbox provides a helper function printTransactionFees()
, which pretty-prints all the values and fees that went into transactions provided. It is quite handy for observing the flow of nanoToncoins.
To use it, modify imports from @ton/sandbox
on top of the test file:
Then, provide an array of transactions as an argument, like so:
To work with individual values of total fees or fees from compute and action phases, inspect each transaction individually:
Transactions with intentional errors
Sometimes it’s useful to make negative tests, featuring intentional errors and throwing specific exit codes.
Example of such Jest test closure in Blueprint:
Note, that to track down transactions with a certain exit code, you only need to specify exitCode
field in object argument to the toHaveTransaction()
method of expect()
.
However, it’s useful to narrow the scope by specifying the recipient address to
, such that Jest would look only at the transaction caused by our message to the contract.
Simulate passage of time
The Unix time in local blockchain instances provided by Sandbox starts at the moment of the creation of those in beforeEach()
block.
Previously, we’ve been warned not to modify the beforeEach()
block unless we really need to. And now, to override the time and time travel a little, we do.
Let’s add the following line by the end of it, setting blockchain.now
explicitly to the time when deployment message was handled:
Now, we can manipulate time in out test clauses. For example, let’s make a transaction one minute after the deployment and another one after two:
Logging via emit
A global static function emit()
sends a message to the outer world — it doesn’t have a specific recipient.
This function is very handy for logging and analyzing data off-chain — one just has to look at external messages produced by the contract.
Logs in local Sandbox tests
When deploying in the Sandbox, you may call emit()
from a receiver function and then observe the list of sent external messages:
Logs of a deployed contract
Every transaction on TON Blockchain contains out_msgs
— a dictionary that holds the list of outgoing messages that were created while executing the transaction.
To see logs from emit()
in that dictionary, look for external messages without a recipient. In various TON Blockchain explorers, such transactions will be marked as external-out
with destination specified as -
or empty
.
Note, that some explorers deserialize the message body sent for you, while others don’t. However, you can always parse it yourself locally.
Parsing body of the emitted message
Consider the following example:
Now, let’s make a simple test clause for the Bonanza
contract:
There, the res
object would contain the list of sent external messages as its externals
field. Let’s access it to parse the first message sent via a call to emit()
in Tact code (or emitted for short):
To parse the second emitted message, we could manually use a bunch of .loadSomething()
functions, but that’s way too brittle — if the fields of the Ballroom
Struct even change, you’d need to start all over. That could really backfire when you have a lot of tests written in that manner.
Fortunately, Tact compiler auto-generates TypeScript bindings (or wrappers) for the contracts, and it’s really easy to re-use them in your test suite. Not only they have a wrapper of the contract you’re testing, but they also export a bunch of helper functions to store or load Structs and Messages defined in the contract. The latter will be named just like the Structs and Messages are, but with the load
prefix in front.
For example, in our case we’ll need a function called loadBallroom()
, for parsing a Slice
into the Ballroom
Struct in TypeScript. To import it, either type the name and let your IDE suggest auto-importing it for you, or take a look at the top of your test suite file — there should be a similar line:
With that, let’s parse the second emitted message:
Mind you, that it’s also possible to parse emitted messages of deployed contracts even outside of our test suite. You would just need to obtain the emitted message bodies and then use the auto-generated TypeScript bindings of Tact alongside the @ton/core
library just like we did in those examples above.
Handling bounced messages
When sent with bounce: true
, messages can bounce back in case of errors. Make sure to write relevant bounced()
message receivers and handle bounced messages gracefully:
Keep in mind that bounced messages in TON have only usable data bits in their message body and don’t have any references, so one cannot recover much data from it. However, you still get to see whether the message has bounced or not, allowing you to create more robust contracts.
Read more about bounced messages and receivers: Bounced messages.
Experimental lab setup
If you’re overwhelmed by the testing setup of Blueprint or just want to test some things quickly, worry not — there is a way to set up a simple playground as an experimental lab to test your ideas and hypotheses.
-
Create a new Blueprint project
That will prevent pollution of your existing one with arbitrary code and tests.
The new project can be named anything, but I’ll name it
Playground
to convey the right intent.To create it, run the following command:
Versions of Blueprint starting with 0.20.0 automatically enable debug mode in
wrappers/
for new contracts, so we only have to adjust the testing suite and prepare ourPlayground
contract for testing. -
Update the test suite
Move into the newly created
tact-playground/
project and in thetests/Playground.spec.ts
, change the"should deploy"
test closure to the following: -
Modify the contract
Replace the code in
contracts/playground.tact
with the following:The basic idea of this setup is to place the code you want to test into the receiver function responding to the string message
"plays"
.Note, that you can still write any valid Tact code outside of that receiver. But in order to test it you’ll need to write related test logic inside of it.
-
Let’s test!
With that, our experimental lab setup is complete. To execute that single test we’ve prepared for our
Playground
contract, run the following:From now on, to test something you only need to modify the contents of the tested receiver function of your Tact contract file and re-run the command above. Rinse and repeat that process until you’ve tested what you wanted to test.
For simplicity and cleaner output’s sake, you may add a new field to
scripts
in yourpackage.json
, such that you’ll only need to runyarn lab
to build and test in one.On Linux or macOS, it would look like:
And here’s how it may look on Windows:
To run: