Processing messages
Summary: In previous steps, we modified our smart contract interaction with
storage
,get methods
, and learned the 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 are used not only for sending currency but also as a 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 the original template project with all modifications performed during this guide here.
Internal messages
Before we proceed to implementation, let's briefly describe the main ways and patterns that we can use to process internal messages.
Actors and roles
Since TON implements the actor model, it's natural to think about smart contract 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 doesn't have a distinct role.owner
: a contract that has exclusive access to some crucial parts of the functionality.
Let's examine the 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
- smart-contract balance at the beggining of the transaction.msg_value
- funds received with message.in_msg_full
-cell
containing "header" fields of the 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 a comprehensive description of sending messages in this section.
What we are specifically interested in is the source address of the message, which we can extract from the msg_full
cell. By obtaining that address and comparing it to a stored one — we can conditionally allow access to crucial parts of our smart contract functionality. A 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
A 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 unleashing the full potential of the TON actors model.
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 the functionality that you want from the external section, the standard way is to delegate this responsibility to a separate actor - wallet, which is practically the main reason they were designed for.
Developing external endpoints includes several standard approaches and security measures that might be overwhelming at this point. Therefore, in this guide, we will implement incrementing the previously added ctx_counter_ext
number.
This implementation is unsafe and may lead to losing your contract funds. Don't deploy it to Mainnet
, especially with a high smart contract balance.
Implementation
Let's modify our smart contract to receive external messages following the standard steps described in the previous Blueprint SDK overview section.
Step 1: edit smart contract code
Add the recv_external
function to the 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 receiving an external message, will increment our ctx_counter_ext
and also send an internal message to the specified address with the increase
operation.
Verify that the smart contract code is correct by running:
npx blueprint build
Expected output should look like this:
✅ Compiled successfully! Cell BOC result:
{
"hash": "310e49288a12dbc3c0ff56113a3535184f76c9e931662ded159e4a25be1fa28b",
"hashBase64": "MQ5JKIoS28PA/1YROjU1GE92yekxZi3tFZ5KJb4foos=",
"hex": "b5ee9c7241010e0100d0000114ff00f4a413f4bcf2c80b01020120020d02014803080202ce0407020120050600651b088831c02456f8007434c0cc1caa42644c383c0040f4c7f4cfcc4060841fa1d93beea5f4c7cc28163c00b817c12103fcbc2000153b513434c7f4c7f4fff4600017402c8cb1fcb1fcbffc9ed548020120090a000dbe7657800b60940201580b0c000bb5473e002b70000db63ffe002606300072f2f800f00103d33ffa40fa00d31f30c8801801cb055003cf1601fa027001cb6a82107e8764ef01cb1f12cb3f5210cb1fc970fb0013a012f0020844ca0a"
}
✅ Wrote compilation artifact to build/HelloWorld.compiled.json