Writing test examples
This page demonstrates how to write tests for FunC contracts using the Blueprint SDK (Sandbox).
The test suites focus on the demo contract Fireworks, a smart contract that starts running with the set_first
message.
When you create a new FunC project using npm create ton@latest
, the SDK automatically generates a test file tests/contract.spec.ts
in the project directory for testing the contract:
import ...
describe('Fireworks', () => {
...
expect(deployResult.transactions).toHaveTransaction({
...
});
});
it('should deploy', async () => {
// The check is done inside beforeEach
// blockchain and fireworks are ready to use
});
Run tests using the following command:
npx blueprint test
You can specify additional options and vmLogs using blockchain.verbosity
:
blockchain.verbosity = {
...blockchain.verbosity,
blockchainLogs: true,
vmLogs: "vm_logs_full",
debugLogs: true,
print: false,
};
Direct unit tests
The Fireworks contract demonstrates different ways of sending messages in the TON Blockchain.
When you deploy the contract with the set_first
message and sufficient TON amount,
it automatically executes with primary and usable combinations of send modes.
The Fireworks contract redeploys itself, creating three entities, each with its ID and, as a result, a different smart contract address.
For clarity, define the Fireworks instances with different state_init
by ID with the following names:
- 1 - Fireworks Setter: This entity spreads different launch opcodes and can be extended to support up to four different opcodes.
- 2 - Fireworks Launcher-1: This Fireworks instance launches the first fireworks, sending messages to the launcher.
- 3 - Fireworks Launcher-2: This Fireworks instance launches the second fireworks, sending messages to the launcher.
Expand details on transactions
Index refers to the ID of a transaction in the launchResult
array.
- 0: An external request to the treasury sends an outbound message
op::set_first
with 2.5 TON to Fireworks. - 1: The Fireworks Setter contract processes a transaction with
op::set_first
, sending two outbound messages to Fireworks Launcher-1 and Fireworks Launcher-2. - 2: Fireworks Launcher-1 processes a transaction with
op::launch_first
, sending four outbound messages to the Launcher. - 3: Fireworks Launcher-2 processes a transaction with
op::launch_second
, sending one outbound message to the Launcher. - 4: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 0
. - 5: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 1
. - 6: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 2
. - 7: The Launcher processes a transaction with an incoming message from Fireworks Launcher-1, sent with
send mode = 128 + 32
. - 8: The Launcher processes a transaction with an incoming message from Fireworks Launcher-2, sent with
send mode = 64
.
Each "firework" is an outbound message with a unique message body, appearing in transactions with IDs 3 and 4.
Below is a list of tests for each transaction expected to execute successfully.
Transaction ID:1 success test
This test verifies that the fireworks are successfully set by sending a transaction with a value of 2.5 TON.
This is the simplest case, in which the main goal is to confirm that the transaction result's success
property is true
.
To filter a specific transaction from the launchResult.transactions
array, you can use the most convenient fields: from
, to
, and op
. This combination retrieves only one transaction.
The transaction[ID:1] in the Fireworks Setter contract is invoked with op::set_first
and executes two outbound messages to Fireworks Launcher-1 and Fireworks Launcher-2.
it("first transaction[ID:1] should set fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launcher.address,
to: fireworks.address,
success: true,
op: Opcodes.set_first,
});
});
Transaction ID:2 success test
This test checks if transaction[ID:2] executes successfully.
The transaction in Fireworks Launcher-1 is invoked with op::launch_first
and executes
four outbound messages to the launcher.
it("should exist a transaction[ID:2] which launches first fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f1.address,
success: true,
op: Opcodes.launch_first,
outMessagesCount: 4,
destroyed: true,
endStatus: "non-existing",
});
printTransactionFees(launchResult.transactions);
});
When a transaction affects the state of a contract, you can specify this using the destroyed
and endStatus
fields.
The complete list of account status-related fields includes:
destroyed
:true
if the existing contract was destroyed due to executing a certain transaction. Otherwise, it isfalse
.deploy
: This custom Sandbox flag indicates whether the contract was deployed during this transaction. It istrue
if the contract was not initialized before this transaction and became initialized afterward. Otherwise, it isfalse
.oldStatus
: AccountStatus before transaction execution. Values:'uninitialized'
,'frozen'
,'active'
,'non-existing'
.endStatus
: AccountStatus after transaction execution. Values:'uninitialized'
,'frozen'
,'active'
,'non-existing'
.
Transaction ID:3 success test
This test checks if transaction[ID:3] executes successfully.
The transaction[ID:3] occurs in Fireworks Launcher-1, is invoked with op::launch_first
,
and executes four outbound messages to the launcher.
it("should exist a transaction[ID:3] which launches second fireworks successfully", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: fireworks.address,
to: launched_f2.address,
success: true,
op: Opcodes.launch_second,
outMessagesCount: 1,
});
printTransactionFees(launchResult.transactions);
});
Transaction ID:4 success test
This test checks if transaction[ID:4] executes successfully.
Transaction[ID:4] occurs in the Launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 0
in the transaction[ID:2].
it("should exist a transaction[ID:4] with a comment send mode = 0", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 0")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:5 success test
This test checks if transaction[ID:5] executes successfully.
Transaction[ID:5] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 1
.
it("should exist a transaction[ID:5] with a comment send mode = 1", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 1")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:6 success test
This test checks if transaction[ID:6] executes successfully.
Transaction[ID:6] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 2
.
it("should exist a transaction[ID:6] with a comment send mode = 2", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 2")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:7 success test
This test checks if transaction[ID:7] executes successfully.
Transaction[ID:7] occurs in the launcher with an incoming message from Fireworks Launcher-1. This message is sent with send mode = 128 + 32
.
it("should exist a transaction[ID:7] with a comment send mode = 32 + 128", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f1.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send mode = 32 + 128")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Transaction ID:8 success test
This test checks if transaction[ID:8] executes successfully.
Transaction[ID:8] occurs in the launcher with an incoming message from Fireworks Launcher-2. This message is sent with send mode = 64
.
it("should exist a transaction[ID:8] with a comment send mode = 64", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
expect(launchResult.transactions).toHaveTransaction({
from: launched_f2.address,
to: launcher.address,
success: true,
body: beginCell()
.storeUint(0, 32)
.storeStringTail("send_mode = 64")
.endCell(), // 0x00000000 comment opcode and encoded comment
});
});
Printing and reading transaction fees
Reading details about fees during testing can help optimize the contract. The printTransactionFees
function prints the entire transaction chain in a convenient format.
it("should execute and print fees", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
console.log(printTransactionFees(launchResult.transactions));
});
For instance, in the case of launchResult
, the following table will be printed:
(index) | op | valueIn | valueOut | totalFees | outActions |
---|---|---|---|---|---|
0 | 'N/A' | 'N/A' | '2.5 TON' | '0.010605 TON' | 1 |
1 | '0x5720cfeb' | '2.5 TON' | '2.185812 TON' | '0.015836 TON' | 2 |
2 | '0x6efe144b' | '1.092906 TON' | '1.081142 TON' | '0.009098 TON' | 4 |
3 | '0xa2e2c2dc' | '1.092906 TON' | '1.088638 TON' | '0.003602 TON' | 1 |
4 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
5 | '0x0' | '0.1 TON' | '0 TON' | '0.000309 TON' | 0 |
6 | '0x0' | '0.099 TON' | '0 TON' | '0.000309 TON' | 0 |
7 | '0x0' | '0.783142 TON' | '0 TON' | '0.000309 TON' | 0 |
8 | '0x0' | '1.088638 TON' | '0 TON' | '0.000309 TON' | 0 |
Index refers to the ID of a transaction in the launchResult
array.
- 0: External request to the treasury that results in a message
op::set_first
to Fireworks. - 1: The Fireworks transaction results in four messages to the launcher.
- 2: Transaction on Launched Fireworks-1 from the Launcher, with a message sent using the
op::launch_first
opcode. - 3: Transaction on Launched Fireworks-2 from the Launcher, with a message sent using the
op::launch_second
opcode. - 4: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 0
. - 5: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 1
. - 6: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 2
. - 7: Transaction on the Launcher with an incoming message from Launched Fireworks-1, sent with
send mode = 128 + 32
. - 8: Transaction on the Launcher with an incoming message from Launched Fireworks-2, sent with
send mode = 64
.
Transaction fees tests
This test verifies whether the transaction fees for launching the fireworks are as expected. You can define custom assertions for different parts of the commission fees.
it("should execute with expected fees", async () => {
const launcher = await blockchain.treasury("launcher");
const launchResult = await fireworks.sendDeployLaunch(
launcher.getSender(),
toNano("2.5")
);
// Total fee
console.log("total fees = ", launchResult.transactions[1].totalFees);
const tx1 = launchResult.transactions[1];
if (tx1.description.type !== "generic") {
throw new Error("Generic transaction expected");
}
// Compute fee
const computeFee =
tx1.description.computePhase.type === "vm"
? tx1.description.computePhase.gasFees
: undefined;
console.log("computeFee = ", computeFee);
// Action fee
const actionFee = tx1.description.actionPhase?.totalActionFees;
console.log("actionFee = ", actionFee);
if (computeFee == null || undefined || actionFee == null || undefined) {
throw new Error("undefined fees");
}
// Check if Compute Phase and Action Phase fees exceed 1 TON
expect(computeFee + actionFee).toBeLessThan(toNano("1"));
});
Edge cases tests
This section provides test cases for TVM exit codes that can occur during transaction processing. These exit codes are part of the blockchain code itself. It is necessary to distinguish between exit codes during the Compute Phase and the Action Phase.
The contract logic is executed during the Compute Phase. Various actions can be created while processing. These actions are processed in the next phase—the Action Phase. If the Compute Phase is unsuccessful, the Action Phase does not start. However, a successful Compute Phase does not guarantee that the Action Phase will also succeed.
Compute Phase | exit code = 0
This exit code indicates that the Compute Phase of the transaction was completed successfully.
Compute Phase | exit code = 1
An alternative exit code for denoting the success of the Compute Phase is 1
. To trigger this exit code, use the RETALT opcode.
This opcode must be called in the main function (e.g., recv_internal
). If called in another function, the exit from that function will be 1
, but the total exit code will be 0
.
Compute Phase | exit code = 2
TVM is a stack machine. When interacting with different values, the system places them on the stack. If an opcode requires elements from the stack but finds it empty, the system throws this error.
This issue can arise when working directly with opcodes, as stdlib.fc assumes this problem will not occur.
Compute Phase | exit code = 3
Before execution, the system converts any code into a continuation
. This special data type includes a slice with code, a stack, registers, and other data required for code execution. If needed, you can run this continuation later, passing the necessary parameters to initialize the stack's state.
First, we build such a continuation. In this case, it is an empty continuation that does nothing. Next, using the opcode 0 SETNUMARGS
, we indicate that no values should be on the stack at the start of execution. Then, we call the continuation using the opcode 1 -1 SETCONTARGS
, passing one value. Since there should have been no values, we get a StackOverflow error.
Compute Phase | exit code = 4
In TVM, integer
values can range from -2256 < x < 2256. If a value exceeds this range during calculation, exit code 4 is thrown.
Compute Phase | exit code = 5
If an integer
value exceeds the expected range, exit code 5 is thrown. For example, if a negative value is used in the .store_uint()
function.
Compute Phase | exit code = 6
At a lower level, opcodes are used instead of familiar function names. In HEX form, these opcodes can be seen in this table. In this example, we use @addop
, which adds a non-existent opcode.
When attempting to process this opcode, the emulator does not recognize it and throws exit code 6.
Compute Phase | exit code = 7
This is a common error that occurs when receiving the wrong data type. In this example, the tuple
contained three elements, but an attempt was made to unpack four.
There are many other cases where this error is thrown, such as:
- Not a null
- Not an integer
- Not a cell
- Not a cell builder
- Not a cell slice
- Not a string
- Not a bytes chunk
- Not a continuation
- Not a box
- Not a tuple
- Not an atom
Compute Phase | exit code = 8
All data in TON is stored in cells. A cell can store up to 1023 bits of data and four references to other cells. If you attempt to write more than 1023 bits or more than four references, exit code 8 is thrown.
Compute Phase | exit code = 9
If you attempt to read more data from a slice than it contains, the system throws exit code 9. For example, this occurs if you try to read 11 bits from a slice containing only 10 bits or attempt to load a reference when no references exist.
Compute Phase | exit code = 10
This error is thrown when working with dictionaries. For example, if the value associated with a key is stored in another cell as a reference, you must use the .udict_get_ref()
function to retrieve the value.
However, the reference to another cell should only be one, not two, as in this example:
root_cell
├── key
│ ├── value
│ └── value - second reference for one key
└── key
└── value
This is why attempting to read the value results in exit code 10.
Additional: You can also store the value next to the key in the dictionary:
root_cell
├── key-value
└── key-value
Note: The actual structure of the dictionary is more complex than shown in the examples above. These examples are simplified for clarity.
Compute Phase | exit code = 11
This error occurs when something unknown happens. For example, when using the SENDMSG opcode, if you pass the wrong (e.g., empty) cell with a message, this error occurs.
It also occurs when attempting to call a non-existent method. Developers often encounter this when calling a non-existent GET method.
Compute Phase | exit code = -14 (13)
The system throws this error if there is insufficient TON to handle the Compute Phase. In the Excno
enum class, which defines exit codes for various Compute Phase errors, the value 13 is specified.
However, during processing, the system applies the NOT operation to this value, changing it to -14
. This ensures that the exit code cannot be faked using functions like throw
, as these functions only accept positive values for exit codes.
Action Phase | exit code = 32
The Action Phase starts after the Compute Phase and processes actions stored in register c5 during the Compute Phase. If the system detects incorrect data in this register, it throws exit code 32.
Action Phase | exit code = 33
Currently, a maximum of 255
actions can be included in one transaction. The Action Phase ends with exit code 33 if this limit is exceeded.
Action Phase | exit code = 34
This exit code covers most errors when working with actions, such as invalid messages or incorrect actions.
Action Phase | exit code = 35
You must specify the correct source address when building a message's CommonMsgInfo part. It must be either addr_none or the address of the account sending the message.
In the blockchain code, this is handled by check_replace_src_addr.
Action Phase | exit code = 36
If the destination address is invalid, exit code 36 is thrown. Possible reasons include a non-existent workchain or an incorrect address. All checks can be seen in check_rewrite_dest_addr.
Action Phase | exit code = 37
This exit code is similar to -14
in the Compute Phase. It indicates insufficient balance to send the specified amount of TON.
Action Phase | exit code = 38
This is similar to exit code 37
but refers to insufficient ExtraCurrency balance.
Action Phase | exit code = 40
If there is enough TON to process part of a message (e.g., 5 cells) but the message contains 10 cells, exit code 40 is thrown.
Action Phase | exit code = 43
This error may occur if the maximum number of cells in the library is exceeded or the maximum depth of the Merkle tree is exceeded.
A library is a cell stored in MasterChain and can be used by all smart contracts if it is public.
Since the order of lines may change when updating the code, some links may become outdated. Therefore, all links reference the code base at commit 9728bc65b75defe4f9dcaaea0f62a22f198abe96.