Skip to main content

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.

tip

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:

() 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.
info

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:

;; 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);
}

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:

;; 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);
}

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.

danger

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:

/contracts/hello_world.fc
() 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 ();
}

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

Step 2: update wrapper

Add a wrapper method to call it through our wrapper class for sending external messages:

/wrappers/HelloWorld.ts
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 the test to ensure that the HelloWorld contract received the external message, and updated its counters:

/tests/HelloWorld.spec.ts
//@version TypeScript 5.8.3
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { Cell, toNano} from '@ton/core';
import { HelloWorld } from '../wrappers/HelloWorld';
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>;

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,
});
});

it('should receive external message and update counter', async () => {
const [__, counterExtBefore] = await helloWorld.getCounters()
const increase = 5;

const result = await helloWorld.sendExternalIncrease({
increaseBy: increase,
value: toNano(0.05),
addr: deployer.address, // Using deployer address
queryID: 0
});

expect(result.transactions).toHaveTransaction({
from: undefined, // External messages have no 'from' address
to: helloWorld.address,
success: true,
});

const [_, counterExt] = await helloWorld.getCounters()
expect(counterExtBefore + BigInt(increase)).toBe(counterExt);
});
});

Verify that all examples are correct by running the test script:

npx blueprint test

Expected output should look like this:

 PASS  tests/HelloWorld.spec.ts
HelloWorld
✓ should receive external message and update counter (251 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.841 s, estimated 2 s
Ran all test suites.

Next step

Now that you understand how smart contracts send and receive messages, you can deploy your contract to TON Blockchain.

Deploying to network

Was this article useful?