Хранение данных и get-методы
Резюме: Ранее мы узнали, как использовать
Blueprint
и как выглядит его структура проекта.
Подробнее с Tact можно ознакомиться в документации Tact и на сайте Tact By Example.
Смарт-контрактам зачастую требуется хранить данные, вроде счётчиков или информации о владении, а также предоставлять способ читать или обновлять их с помощью сообщений. В этом тексте вы узнаете, как определять и инициализировать хранилище контракта, получать и обрабатывать входящие сообщения, а также создавать getter-функции для стения состояния контракта извне блокчейна.
Давайте создадим и изменим наш смарт-контракт в соответствии со стандартными шагами, описанным в предыдущем разделе Обзор Blueprint.
Шаг 1: редактирование кода смарт-контракта
В верхней части сгенерированного файла контракта hello_world.tact
вы можете увидеть определение сообщения:
message Add {
queryId: Int as uint64;
amount: Int as uint32;
}
Сообщение — это структура, которая присылает в контракт данные из другого контракта или извне блокчейна. Tact упрощает работу с сообщениями, автоматически сериализуя их в ячейки TVM и десериализуя из них. Вам не требуется писать низкоуровневый код сериализации или думать о компоновке битов вручную — Tact делает это за вас.
Каждому сообщению назначается уникальный 32-битный идентификатор под названием опкод (сокращение от «код операции»). Этот идентификатор хранится в начале сериализованного сообщения и помогает контракту понимать, какой тип сообщения он получает.
По умолчанию Tact назначает этот идентификатор автоматически. Однако вы также можете определять его вручную, например, когда постепенно развиваете структуру ваших сообщений:
message(0x7e8764ef) Add {
queryId: Int as uint64;
amount: Int as uint32;
}
Tact автоматически сериализует сообщения в ячейки TVM.
При компиляции Tact автоматически сериализирует это сообщение в ячейку TVM. Внутри системы оно окажется представлено примерно так:
begin_cell()
.store_uint(0x7e8764ef, 32) ;; message opcode
.store_uint(query_id, 64)
.store_uint(amount, 32)
.end_cell()
Обычно вам не требуется думать об этом — Tact проделывает всю нужную работу «за кадром». Однако это может быть полезно понимать при отладке или при взаимодействии с низкоуровневыми языками вроде FunC.
Определение контракта
В Tact контракт определяют в объектно-ориентированном стиле программирования:
contract HelloWorld {
...
}
Хранилище контракта
Контракт может хранить переменные состояния, как показано ниже. К ним можно получить доступ с помощью self-ссылки
id: Int as uint32;
counter: Int as uint32;
Эти поля сериализуются аналогично структурам и хранятся в регистре данных контракта.
Используйте self.id
и self.counter
, чтобы обращаться к ним из функций контракта.
Инициализация контракта
Определите функцию init()
, чтобы задать изначальные значения:
init(id: Int) {
self.id = id;
self.counter = 0;
}
Обработка сообщений
Чтобы принимать сообщения от других контрактов, используйте receiver-функцию. Они автоматически вызывают функцию, соответствующую опкоду сообщения:
receive(msg: Add) {
self.counter += msg.amount;
self.notify("Cashback".asComment());
}
Чтобы принимать сообщения с пустым телом, можно добавить receive-функцию
без аргументов:
receive() {
cashback(sender())
}
Getter-функции
Tact поддерживает getter-функции, позволяющие запрашивать состояние контракта извне блокчейна:
Get-функцию нельзя вызвать из другого контракта.
get fun counter(): Int {
return self.counter;
}
Полный код контракта
// 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;
}
}
Проверьте корректность кода смарт-контракта, запустив скрипт сборки:
npx blueprint build
Ожидаемый вывод должен выглядеть примерно так:
✅ Compiled successfully! Cell BOC result:
{
"hash": "cdd26fef4db3a94d735a0431be2f93050c181e6b497346ededea38d8a4a21080",
"hashBase64": "zdJv702zqU1zWgQxvi+TBQwYHmtJc0bt7eo42KSiEIA=",
"hex": "b5ee9c7241020e010001cd00021eff00208e8130e1f4a413f4bcf2c80b010604f401d072d721d200d200fa4021103450666f04f86102f862ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e204925f04e002d70d1ff2e0822182107e8764efba8fab31d33fd31f596c215023db3c03a0884130f84201706ddb3cc87f01ca0055205023cb1fcb1f01cf16c9ed54e001020305040012f8425210c705f2e084001800000000436173686261636b01788210946a98b6ba8eadd33f0131c8018210aff90f5758cb1fcb3fc913f84201706ddb3cc87f01ca0055205023cb1fcb1f01cf16c9ed54e05f04f2c0820500a06d6d226eb3995b206ef2d0806f22019132e21024700304804250231036552212c8cf8580ca00cf8440ce01fa028069cf40025c6e016eb0935bcf819d58cf8680cf8480f400f400cf81e2f400c901fb000202710709014dbe28ef6a268690000cd698fe98ffd202a903609cec08080eb807d202c816880b800f16d9e3618c08000220020378e00a0c014caa18ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e2db3c6c310b000221014ca990ed44d0d200019ad31fd31ffa4055206c139d810101d700fa405902d1017001e2db3c6c310d000222bbeaff01"
}
✅ Wrote compilation artifact to build/HelloWorld.compiled.json
Шаг 2: обновление обёртки
После сборки вашего контракта Tact автоматически генерирует специальный файл обёртки. Обёртки облегчают взаимодействие с контрактом из TypeScript, например, вызов методов или отправку сообщений.
В файле обёртки вы можете увидеть такую строку кода:
export * from '../build/HelloWorld/tact_HelloWorld';
Этот код экспортирует всё из файла tact_HelloWorld.ts
в директорию сборки, делая доступным для использования в других файлах.
Шаг 3: обновление тестов
Обновление тестов
Теперь убедимся, что наш смарт-контракт корректно обновляет счётчик:
- Разверните контракт
HelloWorld
с изначальным значением ID. - Проверьте, что изначальное значение счётчика равно
0
. - Отправьте сообщение
Add
для увеличения счётчика. - Убедитесь, что значение счётчика выросло на ожидаемую сумму.
Реализация теста должна выглядеть следующим образом:
// @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);
});
});
Не забудьте проверить правильность этого примера, запустив тестовый скрипт:
npx blueprint test
Ожидаемый вывод должен выглядеть примерно так:
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.
Следующий шаг
Вы написали свой первый смарт-контракт на языке Tact, протестировали его и изучили, как работают хранилище и get-методы.
Теперь пришло время развёртывания контракта в блокчейне TON.
Развёртывание в сети