Storage and get methods
Summary: In the previous steps, we learned how to use the
Blueprint SDK
and its project structure.
For more details, refer to the Tact documentation and Tact By Example.
Smart contracts often need to store data, like counters or ownership information, and provide a way to read or update it through messages. In this section, you’ll learn how to define and initialize contract storage, receive and handle incoming messages, and create getter functions to read contract states outside the blockchain.
Let's create and modify our smart contract following the standard steps described in the previous Blueprint overview section.
Step 1: edit smart contract code
At the top of the generated contract file: hello_world.tact
, you may see a message definition:
message Add {
queryId: Int as uint64;
amount: Int as uint32;
}
A message is a structure that sends data into a contract from another contract or outside the blockchain. Tact simplifies working with messages by automatically serializing and deserializing them into TVM cells. You don’t need to write low-level serialization code or think about bit layout manually — Tact handles that for you.
Every message is assigned a unique 32-bit identifier called an opcode (short for operation code). This identifier is stored at the beginning of the serialized message and helps the contract understand what type of message it is receiving.
By default, Tact automatically assigns this identifier. However, you can also define it manually, for example, when evolving the structure of your messages over time:
message(0x7e8764ef) Add {
queryId: Int as uint64;
amount: Int as uint32;
}
Tact automatically serializes messages into TVM cells.
When compiled, Tact will automatically serialize this message into a TVM cell. Internally, it would be represented something like this:
begin_cell()
.store_uint(0x7e8764ef, 32) ;; message opcode
.store_uint(query_id, 64)
.store_uint(amount, 32)
.end_cell()
Usually, you don’t need to think about this — Tact takes care of it behind the scenes. However, it can be helpful to understand when debugging or interacting with low-level language like FunC.
Defining the contract
The contract definition in Tact follows an object-oriented programming style:
contract HelloWorld {
...
}
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;
These fields are serialized similarly to structures and stored in the contract's data register.
Use self.id
and self.counter
to access them within contract functions.
Initializing the contract
Define an init()
function to set initial values:
init(id: Int) {
self.id = id;
self.counter = 0;
}
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 receive
function with no arguments:
receive() {
cashback(sender())
}
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;
}
Complete contract
// message with opcode
message(0x7e8764ef) Add {
queryId: Int as uint64;
amount: Int as uint32;
}
// Contract defenition
contract HelloWorld {
// storage variables
id: Int as uint32;
counter: Int as uint32;
// init function.
init(id: Int) {
self.id = id;
self.counter = 0;
}
// default(null) receive for deploy
receive() {
cashback(sender())
}
// function to receive messages from other contracts
receive(msg: Add) {
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/HelloWorld.compiled.json
Step 2: update wrapper
After building your contract, Tact automatically generates a special wrapper file. This wrapper simplifies interaction with your contract from TypeScript, such as calling its methods or sending messages.
In the wrapper file, you'll find this line of code:
export * from '../build/HelloWorld/tact_HelloWorld';
This code exports everything inside the tact_HelloWorld.ts
file in the build folder, making it available for use in other files.
Step 3: updating tests
Updating tests
Now let's ensure that our smart contract correctly updates the counter:
- Deploy the
HelloWorld
contract with an initial ID. - Check that the initial counter value is
0
. - Send an
Add
message to increment the counter. - Verify that the counter value increases by the expected amount.
Implementation of test should look like this:
// @version TypeScript 5.8.3
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { toNano } from '@ton/core';
import { HelloWorld } from '../wrappers/HelloWorld';
import '@ton/test-utils';
describe('HelloWorld Basic Tests', () => {
let blockchain: Blockchain;
let helloWorld: SandboxContract<HelloWorld>;
let sender: SandboxContract<TreasuryContract>;
beforeEach(async () => {
blockchain = await Blockchain.create();
sender = await blockchain.treasury('user');
helloWorld = blockchain.openContract(
// init with id = 0
await HelloWorld.fromInit(0n)
);
const deployResult = await helloWorld.send(
sender.getSender(),
{ value: toNano('1') },
null
);
expect(deployResult.transactions).toHaveTransaction({
from: sender.address,
to: helloWorld.address,
deploy: true,
success: true
});
});
it('should initialize with id = 0 and counter = 0', async () => {
const id = await helloWorld.getId();
const counter = await helloWorld.getCounter();
expect(id).toBe(0n);
expect(counter).toBe(0n);
});
it('should increase counter by given amount', async () => {
const result = await helloWorld.send(
sender.getSender(),
{ value: toNano('0.1') },
{
$$type: 'Add',
queryId: 0n,
amount: 10n
}
);
expect(result.transactions).toHaveTransaction({
from: sender.address,
to: helloWorld.address,
success: true
});
const counter = await helloWorld.getCounter();
expect(counter).toBe(10n);
});
});
Don't forget to verify the example is correct by running test script:
npx blueprint test
Expected output should look like this:
PASS tests/HelloWorld.spec.ts
HelloWorld Basic Tests
✓ should initialize with id = 0 and counter = 0 (305 ms)
✓ should increase counter by given amount (120 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.399 s
Ran all test suites.
Next step
You've written your first smart contract using Tact, tested it, and explored how storage and get methods work.
Now, it's time to deploy the contract to TON Blockchain.
Deploy to the network