跳到主要内容

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:

() 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.
信息

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:

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

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 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:

Project structure
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

At the top of the generated contract file: counter_internal.tact, you may see a message definition:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
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:

/contracts/counter_internal.tact
// 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.

/contracts/counter_internal.tact
get fun counter(): Int {
return self.counter;
}

Note, that the owner getter is automatically defined via the Ownable trait.

Complete contract

/contracts/counter_internal.tact
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:

/wrappers/CounterInternal.ts
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 expected exitCode and counter field remains the same.

Implementation of test should look like this:

/tests/CounterInternal.spec.ts
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:

/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 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:

/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 test to ensure that HelloWorld contract recieved external message, sended internal message to CounterInternal contract and both updated their counters:

/tests/HelloWorld.spec.ts
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:

  1. Send external message to toggle smart contracts logic from outside the blockchain.
  2. Provoke one or more sending of internal messages to other contracts.
  3. 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:

Multi-contract scheme

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.
Was this article useful?