Перейти к основному содержимому

Хранение данных и get-методы

Резюме: Ранее мы узнали, как использовать Blueprint и как выглядит его структура проекта.

к сведению

Подробнее с Tact можно ознакомиться в документации Tact и на сайте Tact By Example.

Смарт-контрактам зачастую требуется хранить данные, вроде счётчиков или информации о владении, а также предоставлять способ читать или обновлять их с помощью сообщений. В этом тексте вы узнаете, как определять и инициализировать хранилище контракта, получать и обрабатывать входящие сообщения, а также создавать getter-функции для стения состояния контракта извне блокчейна.

Давайте создадим и изменим наш смарт-контракт в соответствии со стандартными шагами, описанным в предыдущем разделе Обзор Blueprint.

Шаг 1: редактирование кода смарт-контракта

В верхней части сгенерированного файла контракта hello_world.tact вы можете увидеть определение сообщения:

/contracts/hello_world.tact
message Add {
queryId: Int as uint64;
amount: Int as uint32;
}

Сообщение — это структура, которая присылает в контракт данные из другого контракта или извне блокчейна. Tact упрощает работу с сообщениями, автоматически сериализуя их в ячейки TVM и десериализуя из них. Вам не требуется писать низкоуровневый код сериализации или думать о компоновке битов вручную — Tact делает это за вас.

Каждому сообщению назначается уникальный 32-битный идентификатор под названием опкод (сокращение от «код операции»). Этот идентификатор хранится в начале сериализованного сообщения и помогает контракту понимать, какой тип сообщения он получает.

По умолчанию Tact назначает этот идентификатор автоматически. Однако вы также можете определять его вручную, например, когда постепенно развиваете структуру ваших сообщений:

/contracts/hello_world.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(), чтобы задать изначальные значения:

/contracts/hello_world.tact
init(id: Int) {
self.id = id;
self.counter = 0;
}

Обработка сообщений

Чтобы принимать сообщения от других контрактов, используйте receiver-функцию. Они автоматически вызывают функцию, соответствующую опкоду сообщения:

/contracts/hello_world.tact
receive(msg: Add) {
self.counter += msg.amount;
self.notify("Cashback".asComment());
}

Чтобы принимать сообщения с пустым телом, можно добавить receive-функцию без аргументов:

/contracts/hello_world.tact
receive() {
cashback(sender())
}

Getter-функции

Tact поддерживает getter-функции, позволяющие запрашивать состояние контракта извне блокчейна:

примечание

Get-функцию нельзя вызвать из другого контракта.

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

Полный код контракта

/contracts/hello_world.tact
// 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, например, вызов методов или отправку сообщений.

В файле обёртки вы можете увидеть такую строку кода:

/wrappers/HelloWorld.ts
export * from '../build/HelloWorld/tact_HelloWorld';

Этот код экспортирует всё из файла tact_HelloWorld.ts в директорию сборки, делая доступным для использования в других файлах.

Шаг 3: обновление тестов

Обновление тестов

Теперь убедимся, что наш смарт-контракт корректно обновляет счётчик:

  • Разверните контракт HelloWorldс изначальным значением ID.
  • Проверьте, что изначальное значение счётчика равно 0.
  • Отправьте сообщение Add для увеличения счётчика.
  • Убедитесь, что значение счётчика выросло на ожидаемую сумму.

Реализация теста должна выглядеть следующим образом:

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

Развёртывание в сети

Was this article useful?