Processing Messages
Summary: In previous steps we modified our smart contract interaction with
storage
,get methods
and learned basic smart contract development flow.
Now we are ready to move on to the main functionality of smart contracts - sending and receiving messages. In TON messages not only used for sending currency, but also as data-exchange mechanism between smart contracts making them crucial for smart contract development.
If you are stuck on some of the examples you can find original template project with all modifications performed during this guide here.
Internal messages
Before we proceed to implementation let's briefly describe main ways and patterns that we can use to process internal messages.
Actors and roles
Since TON implements actor model it's natural to think about smart contracts relations in terms of roles
, determining who can access smart contract functionality or not. The most common examples of roles are:
anyone
: any contract that don't have distinct role.owner
: contract that has exclusive access to some crucial parts of functionality.
Let's examine recv_internal
function signature to understand how we could use that:
- FunC
- Tolk
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure
my_balance
- balance of smart contract at the beggining of the transaction.msg_value
- funds recieved with message.in_msg_full
-cell
containing "header" fields of message.in_msg_body
- slice containg payload of the message.
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice)
myBalance
- balance of smart contract at the beggining of the transaction.msgValue
- funds recieved with message.msgFull
-cell
containing "header" fields of message.msgBody
- slice containg payload pf the message.
You can find comprehensive description of sending messages in this section.
What we specifically are interested in is the source address of message that we could extract from msg_full
cell, by obtaining that address and comparing to stored one, that we previously saved, for example, during deployment, we can open crucial part of our smart contract functionality. Common approach looks like this:
- FunC
- Tolk
;; This is NOT a part of the project, just an example.
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; Parse the sender address from in_msg_full
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
;; check if message was send by owner
if (equal_slices_bits(sender_address, owner_address)) {
;;owner operations
return
} else if (equal_slices_bits(sender_address, other_role_address)){
;;other role operations
return
} else {
;;anyone else operations
return
}
;;no known operation were obtained for presented role
;;0xffff is not standard exit code, but is standard practice among TON developers
throw(0xffff);
}
// This is NOT a part of the project, just an example.
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) {
// Parse the sender address from in_msg_full
var cs: slice = msgFull.beginParse();
val flags = cs.loadMessageFlags();
var sender_address = cs~load_msg_address();
if (isSliceBitsEqual(sender_address, owner_address)) {
// owner operations
return
} else if (isSliceBitsEqual(sender_address, other_role_address)){
// other role operations
return
} else {
// anyone else operations
return
}
throw 0xffff; // if the message contains an op that is not known to this contract, we throw
}
Operations
Common pattern in TON contracts is to include a 32-bit operation code in message bodies which tells your contract what action to perform:
- FunC
- Tolk
;; This is NOT a part of the project, just an example!
const int op::increment = 1;
const int op::decrement = 2;
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
;; Step 1: Check if the message is empty
if (in_msg_body.slice_empty?()) {
return; ;; Nothing to do with empty messages
}
;; Step 2: Extract the operation code
int op = in_msg_body~load_uint(32);
;; Step 3-7: Handle the requested operation
if (op == op::increment) {
increment(); ;;call to specific operation handler
return;
} else if (op == op::decrement) {
decrement();
;; Just accept the money
return;
}
;; Unknown operation
throw(0xffff);
}
//This is NOT a part of the project, just an example!
const op::increment : int = 1;
const op::decrement : int = 2;
fun onInternalMessage(myBalance: int, msgValue: int, msgFull: cell, msgBody: slice) {
// Step 1: Check if the message is empty
if (slice.isEndOfSlice()) {
return; // Nothing to do with empty messages
}
// Step 2: Extract the operation code
var op = in_msg_body~load_uint(32);
// Step 3-7: Handle the requested operation
if (op == op::increment) {
increment(); //call to specific operation handler
return;
} else if (op == op::decrement) {
decrement();
// Just accept the money
return;
}
// Unknown operation
throw(0xffff);
}
By combining both of these patterns you can achieve a comprehensive description of your smart contract's systems ensuring secure interaction between them and unleash full potential of TON actors model.
Implementation in Tact
Tact is a high-level programming language for the TON Blockchain, focused on efficiency and simplicity. It is designed to be easy to learn and use while being well-suited for smart contract development. Tact is a statically typed language with a simple syntax and a powerful type system.
For more details, refer to the Tact documentation and Tact By Example.
Let's create and modify our smart contract folowing standard steps decsribed in previous Blueprint SDK overview section.
Step 1: Creating and modifying Tact contract
First, let's create Tact contract in the same directory as previous one.
npx blueprint create CounterInternal --type tact-counter
Resulted project structure should look like this:
- FunC
- Tolk
Example/
├─ contracts/ # Smart contract source code
│ ├─ counter_internal.tact # CounterInternal contract in Tact language
│ ├─ hello_world.fc # HelloWorld contract in FunC language
├─ scripts/ # Deployment and interaction scripts
│ ├─ deployCounterInternal.ts # Script to deploy contract
│ ├─ deployHelloWorld.ts # Script to deploy contract
│ ├─ incrementCounterInternal.ts # Script to increment counter
│ └─ incrementHelloWorld.ts # Script to increment counter
├─ tests/ # Test suite for contracts
│ ├─ CounterInternal.spec.ts # Tests for CounterInternal contract
│ └─ HelloWorld.spec.ts # Tests for HelloWorld contract
└─ wrappers/ # Wrappers for contract interaction
├─ CounterInternal.compile.ts # Compilation configuration
├─ CounterInternal.ts # Wrapper for CounterInternal contract
├─ HelloWorld.compile.ts # Compilation configuration
└─ HelloWorld.ts # Wrapper for HelloWorld contract
Example/
├─ contracts/ # Smart contract source code
│ ├─ counter_internal.tact # CounterInternal contract in Tact language
│ ├─ hello_world.tolk # HelloWorld contract in FunC language
├─ scripts/ # Deployment and interaction scripts
│ ├─ deployCounterInternal.ts # Script to deploy contract
│ ├─ deployHelloWorld.ts # Script to deploy contract
│ ├─ incrementCounterInternal.ts # Script to increment counter
│ └─ incrementHelloWorld.ts # Script to increment counter
├─ tests/ # Test suite for contracts
│ ├─ CounterInternal.spec.ts # Tests for CounterInternal contract
│ └─ HelloWorld.spec.ts # Tests for HelloWorld contract
└─ wrappers/ # Wrappers for contract interaction
├─ CounterInternal.compile.ts # Compilation configuration
├─ CounterInternal.ts # Wrapper for CounterInternal contract
├─ HelloWorld.compile.ts # Compilation configuration
└─ HelloWorld.ts # Wrapper for HelloWorld contract
At the top of the generated contract file: counter_internal.tact
, you may see a message definition:
message Add {
queryId: Int as uint64;
amount: Int as uint32;
}
A message is a basic structure for communication between contracts. Tact automatically serializes and deserializes messages into cells. To ensure that opcodes will be the same during message structure changes, it may be added like below:
message(0x7e8764ef) Add {
queryId: Int as uint64;
amount: Int as uint32;
}
Tact will serialize it as follows:
begin_cell()
.store_uint(0x7e8764ef, 32) ;; message opcode
.store_uint(query_id, 64)
.store_uint(amount, 32)
.end_cell()
Using this structure, a message can be sent to the contract from FunC.
Defining the contract
The contract definition in Tact follows an object-oriented programming style:
contract CounterInternal {
...
}
Contract storage
A contract may store its state variables as follows. They may be accessed with Self reference
id: Int as uint32;
counter: Int as uint32;
To ensure that only the contract owner can interact with specific functions, add an owner
field:
id: Int as uint32;
counter: Int as uint32;
owner: Address;
These fields are serialized similarly to structures and stored in the contract's data register.
Initializing the contract
If you compile the contract at this stage, you will encounter the error: Field "owner" is not set
. This is because the contract needs to initialize its fields upon deployment. Define an init()
function to do this:
init(id: Int, owner: Address) {
self.id = id;
self.counter = 0;
self.owner = owner;
}
Handling messages
To accept messages from other contracts, use a receiver function. Receiver functions automatically match the message's opcode and invoke the corresponding function:
receive(msg: Add) {
self.counter += msg.amount;
self.notify("Cashback".asComment());
}
For accepting messages with empty body you can add recieve
function with no arguments:
receive() {
cashback(sender())
}
Restricting access
Tact also provides handy ways to share same logic through traits. To ensure that only the contract owner can send messages use the Ownable
trait, which provides built-in ownership checks:
// Import library to use trait
import "@stdlib/ownable";
// Ownable trait introduced here
contract CounterInternal with Ownable {
...
receive(msg: Add) {
self.requireOwner();
self.counter += msg.amount;
self.notify("Cashback".asComment());
}
}
Identity validation plays a key role in secure contract interactions. You can read more about identity validation and its importance in the linked documentation.
Getter functions
Tact supports getter functions for retrieving contract state off-chain:
Get function cannot be called from another contract.
get fun counter(): Int {
return self.counter;
}
Note, that the owner
getter is automatically defined via the Ownable
trait.
Complete contract
import "@stdlib/ownable";
// message with opcode
message(0x7e8764ef) Add {
queryId: Int as uint64;
amount: Int as uint32;
}
// Contract defenition. `Ownable` is a trait to share functionality.
contract CounterInternal with Ownable {
// storage variables
id: Int as uint32;
counter: Int as uint32;
owner: Address;
// init function.
init(id: Int, owner: Address) {
self.id = id;
self.counter = 0;
self.owner = owner;
}
// default(null) recieve for deploy
receive() {
cashback(sender())
}
// function to recive messages from other contracts
receive(msg: Add) {
// function from `Ownable` trait to assert, that only owner may call this
self.requireOwner();
self.counter += msg.amount;
// Notify the caller that the receiver was executed and forward remaining value back
self.notify("Cashback".asComment());
}
// getter function to be called offchain
get fun counter(): Int {
return self.counter;
}
get fun id(): Int {
return self.id;
}
}
Verify that smart contract code is correct by running build script:
npx blueprint build
Expected output should look like this:
✅ Compiled successfully! Cell BOC result:
{
"hash": "cdd26fef4db3a94d735a0431be2f93050c181e6b497346ededea38d8a4a21080",
"hashBase64": "zdJv702zqU1zWgQxvi+TBQwYHmtJc0bt7eo42KSiEIA=",
"hex": "b5ee9c7241020e010001cd00021eff00208e8130e1f4a413f4bcf2c80b010604f401d072d721d200d200fa4021103450666f04f86102f862ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e204925f04e002d70d1ff2e0822182107e8764efba8fab31d33fd31f596c215023db3c03a0884130f84201706ddb3cc87f01ca0055205023cb1fcb1f01cf16c9ed54e001020305040012f8425210c705f2e084001800000000436173686261636b01788210946a98b6ba8eadd33f0131c8018210aff90f5758cb1fcb3fc913f84201706ddb3cc87f01ca0055205023cb1fcb1f01cf16c9ed54e05f04f2c0820500a06d6d226eb3995b206ef2d0806f22019132e21024700304804250231036552212c8cf8580ca00cf8440ce01fa028069cf40025c6e016eb0935bcf819d58cf8680cf8480f400f400cf81e2f400c901fb000202710709014dbe28ef6a268690000cd698fe98ffd202a903609cec08080eb807d202c816880b800f16d9e3618c08000220020378e00a0c014caa18ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e2db3c6c310b000221014ca990ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e2db3c6c310d000222bbeaff01"
}
✅ Wrote compilation artifact to build/CounterInternal.compiled.json
Step 2: Update wrapper
Wrappers facilitate contract interaction from TypeScript. Unlike in FunC or Tolk, they are generated automatically during the build process:
export * from '../build/CounterInternal/tact_CounterInternal';
Step 3: Updating tests
Now let's ensure that our smart contract code fails when we try to send add
message from non-owner:
- Create
CounterInternal
with some owner. - Create another smart contract that will have different address -
nonOwner
. - Try to send an internal message to
CounterInternal
and enusre that it fails with expectedexitCode
and counter field remains the same.
Implementation of test should look like this:
import { Blockchain, SandboxContract, TreasuryContract} from '@ton/sandbox';
import { Cell, toNano } from '@ton/core';
import { CounterInternal } from '../wrappers/CounterInternal';
import '@ton/test-utils';
import { compile } from '@ton/blueprint';
describe('CounterInternal', () => {
let codeHelloWorld: Cell;
let blockchain: Blockchain;
let counterInternal: SandboxContract<CounterInternal>;
let deployerCounter: SandboxContract<TreasuryContract>;
beforeAll(async () => {
codeHelloWorld = await compile('HelloWorld');
});
beforeEach(async () => {
blockchain = await Blockchain.create();
deployerCounter = await blockchain.treasury('deployerCounter');
// Deploy CounterInternal with deployer as the owner
counterInternal = blockchain.openContract(
await CounterInternal.fromInit(0n, deployerCounter.address)
);
const deployResultCounter = await counterInternal.send(
deployerCounter.getSender(),
{ value: toNano('1.00') },
null
);
expect(deployResultCounter.transactions).toHaveTransaction({
from: deployerCounter.address,
to: counterInternal.address,
deploy: true,
success: true
});
});
it('should fail if not owner call increment', async () => {
// Get initial counter value
const counterBefore = await counterInternal.getCounter();
// Try to increase counter from a non-owner account (should fail)
const nonOwner = await blockchain.treasury('nonOwner');
const increaseBy = 5n;
const nonOwnerResult = await counterInternal.send(nonOwner.getSender(), {
value: toNano('0.05')
}, {
$$type: 'Add',
amount: increaseBy,
queryId: 0n
});
// This should fail since only the owner should be able to increment
expect(nonOwnerResult.transactions).toHaveTransaction({
from: nonOwner.address,
to: counterInternal.address,
success: false,
exitCode: 132 // The error code thrown in the contract, Access Denied
});
// Counter should remain unchanged
const counterAfterNonOwner = await counterInternal.getCounter();
expect(counterAfterNonOwner).toBe(counterBefore);
});
});
Don't forget to verify that all examples is correct by running test script:
npx blueprint test
Expected output should look like this:
PASS tests/CounterInternal.spec.ts
CounterInternal
✓ should fail if not owner call increment (506 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.387 s, estimated 6 s
Ran all test suites matching /CounterInternal/i.
External Messages
External messages
are your only way of toggling smart contract logic from outside the blockchain. Usually, there is no need for implementation of them in smart contracts because in most cases you don't want external entry points to be accessible to anyone except you. If this is all functionality that you want from external section - standard way is to delegate this responsibility to separate actor - wallet, which is practically the main reason they were designed for.
Developing external endpoint includes several standard approuches and security measures that might be overwhelming at this point. Therefore in this guide we will realize incrementing previously added to hello_world
contract ctxCounterExt
number and add send message to out Tact
contract.
This implementation is unsafe and may lead to loosing your contract funds. Don't deploy it to Mainnet
, especially with high smart contract balance.
Implementation
Let's modify our smart contract to recieve external messages folowing standard steps decsribed in previous Blueprint SDK overview section.
Step1 : Edit smart contract code
Add recv_external
function to HelloWorld
smart contract:
- FunC
- Tolk
() recv_external(slice in_msg) impure {
accept_message();
var (ctx_id, ctx_counter, ctx_counter_ext) = load_data();
var query_id = in_msg~load_uint(64);
var addr = in_msg~load_msg_addr();
var coins = in_msg~load_coins();
var increase_by = in_msg~load_uint(32);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(coins)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op::increase, 32)
.store_uint(query_id, 64)
.store_uint(increase_by, 32);
send_raw_message(msg.end_cell(), 0);
ctx_counter_ext += increase_by;
save_data(ctx_id, ctx_counter, ctx_counter_ext);
return ();
}
fun acceptExternalMessage(): void
asm "ACCEPT";
fun onExternalMessage(inMsg: slice) {
acceptExternalMessage();
var (ctxId, ctxCounter, ctxCounterExt) = loadData();
var queryId = inMsg.loadUint(64);
var addr = inMsg.loadAddress();
var coins = inMsg.loadCoins();
var increaseBy = inMsg.loadUint(32);
var msg = beginCell()
.storeUint(0x18, 6)
.storeSlice(addr)
.storeCoins(coins)
.storeUint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.storeUint(OP_INCREASE, 32)
.storeUint(queryId, 64)
.storeUint(increaseBy, 32);
sendRawMessage(msg.endCell(), 0);
ctxCounterExt += increaseBy;
saveData(ctxId, ctxCounter, ctxCounterExt);
return ();
}
This function upon recieving an external message will increment our ctxCounter
and also send an internal message to specified address with increase
operation that we will use to increment counter on our CounterInternal
smart contract.
Verify that smart contract code is correct by running:
npx blueprint build
Expected should look like this:
✅ Compiled successfully! Cell BOC result:
{
"hash": "310e49288a12dbc3c0ff56113a3535184f76c9e931662ded159e4a25be1fa28b",
"hashBase64": "MQ5JKIoS28PA/1YROjU1GE92yekxZi3tFZ5KJb4foos=",
"hex": "b5ee9c7241010e0100d0000114ff00f4a413f4bcf2c80b01020120020d02014803080202ce0407020120050600651b088831c02456f8007434c0cc1caa42644c383c0040f4c7f4cfcc4060841fa1d93beea5f4c7cc28163c00b817c12103fcbc2000153b513434c7f4c7f4fff4600017402c8cb1fcb1fcbffc9ed548020120090a000dbe7657800b60940201580b0c000bb5473e002b70000db63ffe002606300072f2f800f00103d33ffa40fa00d31f30c8801801cb055003cf1601fa027001cb6a82107e8764ef01cb1f12cb3f5210cb1fc970fb0013a012f0020844ca0a"
}
✅ Wrote compilation artifact to build/HelloWorld.compiled.json
Step 2: Update Wrapper
And add wrapper method to call it through our wrapper class for sending external message:
async sendExternalIncrease(
provider: ContractProvider,
opts: {
increaseBy: number;
value: bigint;
addr: Address;
queryID?: number;
}
) {
const message = beginCell()
.storeUint(opts.queryID ?? 0, 64)
.storeAddress(opts.addr)
.storeCoins(opts.value)
.storeUint(opts.increaseBy, 32)
.endCell()
return await provider.external(message);
}
Step 3: Update test
Update test to ensure that HelloWorld
contract recieved external message, sended internal message to CounterInternal
contract and both updated their counters:
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { Cell, toNano} from '@ton/core';
import { HelloWorld } from '../wrappers/HelloWorld';
import { CounterInternal } from '../wrappers/CounterInternal';
import '@ton/test-utils';
import { compile } from '@ton/blueprint';
describe('HelloWorld', () => {
let code: Cell;
beforeAll(async () => {
code = await compile('HelloWorld');
});
let blockchain: Blockchain;
let deployer: SandboxContract<TreasuryContract>;
let helloWorld: SandboxContract<HelloWorld>;
let counterInternal: SandboxContract<CounterInternal>;
beforeEach(async () => {
blockchain = await Blockchain.create();
helloWorld = blockchain.openContract(
HelloWorld.createFromConfig(
{
id: 0,
ctxCounter: 0,
ctxCounterExt: 0n,
},
code
)
);
deployer = await blockchain.treasury('deployer');
const deployResult = await helloWorld.sendDeploy(deployer.getSender(), toNano('1.00'));
expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: helloWorld.address,
deploy: true,
success: true,
});
counterInternal = blockchain.openContract(
await CounterInternal.fromInit(0n, helloWorld.address)
);
const deployResultCounter = await counterInternal.send(
deployer.getSender(),
{ value: toNano('1.00') },
null
);
expect(deployResultCounter.transactions).toHaveTransaction({
from: deployer.address,
to: counterInternal.address,
deploy: true,
success: true
});
});
it('should send an external message and update counter', async () => {
const counterInternalValueBefore = await counterInternal.getCounter();
const [__, counterExtBefore] = await helloWorld.getCounters()
const increase = 5;
const result = await helloWorld.sendExternalIncrease({
increaseBy: increase,
value: toNano(0.05),
addr: counterInternal.address,
queryID: 0
});
expect(result.transactions).toHaveTransaction({
from: undefined, // External messages have no 'from' address
to: helloWorld.address,
success: true,
});
expect(result.transactions).toHaveTransaction({
from: helloWorld.address,
to: counterInternal.address,
success: true,
});
const counterInternalValueAfter = await counterInternal.getCounter();
expect(counterInternalValueAfter).toBeGreaterThan(counterInternalValueBefore);
const [_, counterExt] = await helloWorld.getCounters()
expect(counterExtBefore + BigInt(increase)).toBe(counterExt);
});
// ... previous tests
});
This test describes common trascantion flow of any multi-contract
system:
- Send external message to toggle smart contracts logic from outside the blockchain.
- Provoke one or more sending of internal messages to other contracts.
- Upon recieving an internal message change contract state and repeat step 2 if required.
Since resulting sequence of transactions might be overwhelming for understanding, it's a good practice to make a sequence diagram
describing your system, here is an example of our case:

Verify that all examples is correct by running test script:
npx blueprint test
Expected output should look like this:
PASS tests/HelloWorld.spec.ts
HelloWorld
✓ should correctly initialize and return the initial data (446 ms)
✓ should send an external message and update counter (209 ms)
✓ should increase counter (298 ms)
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 3.663 s, estimated 4 s
Ran all test suites matching /HelloWorld/i.