import { Address, beginCell, Cell, Contract, contractAddress } from "ton-core";
export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);
return new MainContract(address, init);
}
}
Наш метод createFromConfig принимает параметр config (пока игнорируйте), codeкоторый представляет собой ячейку со скомпилированным кодом нашего контракта и workchain, которая определяет рабочую цепочку TON, в которую должен быть помещен контракт. В настоящее время существует только одна рабочая цепочка на ton - 0.
Чтобы лучше понять этот код, давайте начнем с результатов, которые мы возвращаем. Мы создаем новый экземпляр нашего класса MainContract, и, как определено в его конструкторе, мы должны передать ему будущий address контракта (который мы можем вычислить, как отмечалось ранее) и init начальное состояние контракта.
Адрес, который мы вычисляем с помощью функции, которую мы импортируем из библиотеки ton-core. Мы передаем параметры состояния workchain и init, чтобы получить его.
Состояние init - это просто объект со свойствами code и data. Место, где передается code, передается в наш метод, а data пока представляют собой просто пустую ячейку. Мы узнаем, как преобразовать config данные в формат ячейки, чтобы мы могли использовать config данные в состоянии init нашего контракта.
Подводя итог, createFromConfig принимает config с данными, которые мы в будущем сохраним в постоянном хранилище контракта, и code контракта. Взамен мы получаем экземпляр контракта, с которым мы можем легко взаимодействовать с помощью sandbox.
Давайте вернемся к нашим tests/main.spec.ts и выполним следующие шаги:
- импортируем нашу оболочку
- импортируем скомпилированный код контракта в виде шестнадцатеричного представления
- преобразуем его в ячейку
- используем openContract sandbox вместе с нашей новой оболочкой, чтобы получить экземпляр контракта, с которым мы, наконец, сможем взаимодействовать
import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
describe("main.fc contract tests", () => {
it("our first test", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];
const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);
});
});
На данный момент у нас есть экземпляр смарт-контракта, с которым мы можем взаимодействовать во многом аналогично реальному контракту, чтобы протестировать ожидаемое поведение.
export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);
return new MainContract(address, init);
}
}
Наш метод createFromConfig принимает параметр config (пока игнорируйте), codeкоторый представляет собой ячейку со скомпилированным кодом нашего контракта и workchain, которая определяет рабочую цепочку TON, в которую должен быть помещен контракт. В настоящее время существует только одна рабочая цепочка на ton - 0.
Чтобы лучше понять этот код, давайте начнем с результатов, которые мы возвращаем. Мы создаем новый экземпляр нашего класса MainContract, и, как определено в его конструкторе, мы должны передать ему будущий address контракта (который мы можем вычислить, как отмечалось ранее) и init начальное состояние контракта.
Адрес, который мы вычисляем с помощью функции, которую мы импортируем из библиотеки ton-core. Мы передаем параметры состояния workchain и init, чтобы получить его.
Состояние init - это просто объект со свойствами code и data. Место, где передается code, передается в наш метод, а data пока представляют собой просто пустую ячейку. Мы узнаем, как преобразовать config данные в формат ячейки, чтобы мы могли использовать config данные в состоянии init нашего контракта.
Подводя итог, createFromConfig принимает config с данными, которые мы в будущем сохраним в постоянном хранилище контракта, и code контракта. Взамен мы получаем экземпляр контракта, с которым мы можем легко взаимодействовать с помощью sandbox.
Давайте вернемся к нашим tests/main.spec.ts и выполним следующие шаги:
- импортируем нашу оболочку
- импортируем скомпилированный код контракта в виде шестнадцатеричного представления
- преобразуем его в ячейку
- используем openContract sandbox вместе с нашей новой оболочкой, чтобы получить экземпляр контракта, с которым мы, наконец, сможем взаимодействовать
import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
describe("main.fc contract tests", () => {
it("our first test", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];
const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);
});
});
На данный момент у нас есть экземпляр смарт-контракта, с которым мы можем взаимодействовать во многом аналогично реальному контракту, чтобы протестировать ожидаемое поведение.
GitHub
GitHub - ton-org/sandbox: Local TON emulator
Local TON emulator. Contribute to ton-org/sandbox development by creating an account on GitHub.
Взаимодействие с контрактом
Библиотека ton-core предоставляет нам еще одну замечательную конструкцию под названием Address . Как вы знаете, у каждого объекта в блокчейне TON есть адрес. В реальной жизни, если вы хотите отправить сообщение из одного контракта (например, контракта кошелька) в другой, вы знаете адреса обоих контрактов.
При написании тестов мы эмулируем контракт, а при взаимодействии — известен адрес нашего эмулируемого контракта. Однако нам понадобится кошелек, который мы будем использовать для развертывания нашего контракта, и еще один для взаимодействия с нашим контрактом.
Это sandbox делается таким образом, что мы вызываем treasure метод экземпляра блокчейна и предоставляем ему начальную фразу: const senderWallet = await blockchain.treasury("sender");
Эмуляция внутреннего сообщения
Вернемся к написанию нашего теста. Поскольку у нас есть экземпляр нашего контракта, давайте отправим ему внутреннее сообщение, чтобы наш адрес отправителя был сохранен в хранилище c4. Мы помним, что для взаимодействия с нашим экземпляром контракта нам нужно использовать обертку. Давайте вернемся к нашим файлам- wrappers/MainContract.ts и создадим в нашей оболочке новый метод под названием sendInternalMessage .
Изначально это будет выглядеть так:
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
){
}
Наш новый метод получает параметр типа ContractProvider , отправителя сообщения типа Sender и значение сообщения value .
Обычно нам не нужно беспокоиться о передаче ContractProvider при использовании этого метода, он будет передаваться «под капотом» как часть экземпляра контракта, встроенного в функциональные возможности. Однако не забудьте импортировать эти типы из библиотеки ton-core .
Давайте реализуем логику отправки внутреннего сообщения внутри нашего нового метода. Это будет выглядеть так:
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
Вы можете видеть, что мы используем provider и вызываем его метод под названием Internal . Мы передаем отправителя sender в качестве первого параметра, затем формируем объект с аргументами, который включает в себя:
- value сообщения (количество TON в формате nano)
- sendMode — мы используем перечисление SendMode, предоставленное ton-core, вы можете узнать больше о том, как эти режимы работают «под капотом», на странице документации , посвященной этому.
- body , которое должно быть ячейкой с телом сообщения, но мы пока оставляем его пустой ячейкой
Пожалуйста, не забудьте импортировать все новые типы и объекты, которые мы используем, из ton-coreбиблиотеки.
Итак, наш окончательный код wrappers/main выглядит так:
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from "ton-core";
export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);
return new MainContract(address, init);
}
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
}
Вызов этого метода в нашем tests/main.spec.ts будет выглядеть так:
const senderWallet = await blockchain.treasury("sender");
myContract.sendInternalMessage(senderWallet.getSender(), toNano("0.05"));
Обратите внимание, как мы используем вспомогательную функцию toNano ton-core , импортированную из библиотеки, для преобразования строкового значения в формат nano bignum.
Библиотека ton-core предоставляет нам еще одну замечательную конструкцию под названием Address . Как вы знаете, у каждого объекта в блокчейне TON есть адрес. В реальной жизни, если вы хотите отправить сообщение из одного контракта (например, контракта кошелька) в другой, вы знаете адреса обоих контрактов.
При написании тестов мы эмулируем контракт, а при взаимодействии — известен адрес нашего эмулируемого контракта. Однако нам понадобится кошелек, который мы будем использовать для развертывания нашего контракта, и еще один для взаимодействия с нашим контрактом.
Это sandbox делается таким образом, что мы вызываем treasure метод экземпляра блокчейна и предоставляем ему начальную фразу: const senderWallet = await blockchain.treasury("sender");
Эмуляция внутреннего сообщения
Вернемся к написанию нашего теста. Поскольку у нас есть экземпляр нашего контракта, давайте отправим ему внутреннее сообщение, чтобы наш адрес отправителя был сохранен в хранилище c4. Мы помним, что для взаимодействия с нашим экземпляром контракта нам нужно использовать обертку. Давайте вернемся к нашим файлам- wrappers/MainContract.ts и создадим в нашей оболочке новый метод под названием sendInternalMessage .
Изначально это будет выглядеть так:
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
){
}
Наш новый метод получает параметр типа ContractProvider , отправителя сообщения типа Sender и значение сообщения value .
Обычно нам не нужно беспокоиться о передаче ContractProvider при использовании этого метода, он будет передаваться «под капотом» как часть экземпляра контракта, встроенного в функциональные возможности. Однако не забудьте импортировать эти типы из библиотеки ton-core .
Давайте реализуем логику отправки внутреннего сообщения внутри нашего нового метода. Это будет выглядеть так:
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
Вы можете видеть, что мы используем provider и вызываем его метод под названием Internal . Мы передаем отправителя sender в качестве первого параметра, затем формируем объект с аргументами, который включает в себя:
- value сообщения (количество TON в формате nano)
- sendMode — мы используем перечисление SendMode, предоставленное ton-core, вы можете узнать больше о том, как эти режимы работают «под капотом», на странице документации , посвященной этому.
- body , которое должно быть ячейкой с телом сообщения, но мы пока оставляем его пустой ячейкой
Пожалуйста, не забудьте импортировать все новые типы и объекты, которые мы используем, из ton-coreбиблиотеки.
Итак, наш окончательный код wrappers/main выглядит так:
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from "ton-core";
export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
static createFromConfig(config: any, code: Cell, workchain = 0) {
const data = beginCell().endCell();
const init = { code, data };
const address = contractAddress(workchain, init);
return new MainContract(address, init);
}
async sendInternalMessage(
provider: ContractProvider,
sender: Sender,
value: bigint
) {
await provider.internal(sender, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
}
Вызов этого метода в нашем tests/main.spec.ts будет выглядеть так:
const senderWallet = await blockchain.treasury("sender");
myContract.sendInternalMessage(senderWallet.getSender(), toNano("0.05"));
Обратите внимание, как мы используем вспомогательную функцию toNano ton-core , импортированную из библиотеки, для преобразования строкового значения в формат nano bignum.
Вызов метода получения
Нам нужно будет создать еще один метод в оболочке нашего контракта, а именно — метод getData , который будет запускать метод получения нашего контракта и возвращать результаты, которые мы имеем в хранилище c4.
Вот как будет выглядеть наш метод получения:
async getData(provider: ContractProvider) {
const { stack } = await provider.get("get_the_latest_sender", []);
return {
recent_sender: stack.readAddress(),
};
}
Как и в нашем случае с отправкой внутреннего сообщения — мы используем провайдера и его методы. В данном случае мы используем метод get . Затем мы читаем адрес из полученного stack и в результате возвращаем его.
Написание тестов
Вот и все. Мы выполнили всю подготовительную работу и теперь напишем собственно тестовую логику. Вот как будет выглядеть наш тестовый сценарий:
1. Отправляем и внутреннее сообщение
2. Мы гарантируем, что отправка прошла успешно
3. Мы вызываем метод получения контракта и проверяем, что вызов прошел успешно.
4. Мы сравниваем результаты, полученные от метода получения, с адресом from , который мы установили в исходном внутреннем сообщении.
Кажется довольно простым и выполнимым! Давай сделаем это.
Команда Sandbox предоставила нам еще одну замечательную утилиту для тестирования. Мы можем установить дополнительный пакет @ton-community/test-utils, запустив yarn add @ton-community/test-utils -D
Это позволит нам использовать .toHaveTransaction для средства сопоставления jest, чтобы добавить дополнительных помощников для упрощения тестирования. Нам также нужно будет импортировать этот пакет в наш test/main.spec.ts после установки.
Давайте посмотрим, как выглядит наш тестовый код, основанный на описанном выше сценарии.
import { Cell, toNano } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import "@ton-community/test-utils";
describe("main.fc contract tests", () => {
it("should get the proper most recent sender address", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];
const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);
const senderWallet = await blockchain.treasury("sender");
const sentMessageResult = await myContract.sendInternalMessage(
senderWallet.getSender(),
toNano("0.05")
);
expect(sentMessageResult.transactions).toHaveTransaction({
from: senderWallet.address,
to: myContract.address,
success: true,
});
const data = await myContract.getData();
expect(data.recent_sender.toString()).toBe(senderWallet.address.toString());
});
});
Мы используем функциональность Jest, чтобы убедиться, что:
- отправка внутреннего сообщения прошла успешно
- метод получения прошел успешно
- метод получения возвращает некоторый результат
- адрес, возвращаемый методом получения, равен тому, который мы использовали в нашем сообщении, устанавливая адрес отправителя
Мы также переименовали «наш первый тест» в «должен получить правильный последний адрес отправителя», мы хотим, чтобы имена наших тестов всегда были читабельными.
Запуск тестов
Вуаля! Мы готовы провести наши тесты. Просто запустите нашу команду yarn testв терминале, и если вы все сделали вместе со мной, вы получите аналогичный результат:
PASS tests/main.spec.ts
main.fc contract tests
✓ should get the proper most recent sender address (444 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.13 s, estimated 5 s
И последнее, что мы хотим сделать, это убедиться, что каждый раз, когда мы запускаем тесты, мы также запускаем наш сценарий компилятора. Это полезно для эффективности нашей работы. Большая часть работы по разработке заключается в написании функционального кода и последующем запуске тестов. Давайте упростим это:
Обновите файл package.json , чтобы он выглядел следующим образом:
Нам нужно будет создать еще один метод в оболочке нашего контракта, а именно — метод getData , который будет запускать метод получения нашего контракта и возвращать результаты, которые мы имеем в хранилище c4.
Вот как будет выглядеть наш метод получения:
async getData(provider: ContractProvider) {
const { stack } = await provider.get("get_the_latest_sender", []);
return {
recent_sender: stack.readAddress(),
};
}
Как и в нашем случае с отправкой внутреннего сообщения — мы используем провайдера и его методы. В данном случае мы используем метод get . Затем мы читаем адрес из полученного stack и в результате возвращаем его.
Написание тестов
Вот и все. Мы выполнили всю подготовительную работу и теперь напишем собственно тестовую логику. Вот как будет выглядеть наш тестовый сценарий:
1. Отправляем и внутреннее сообщение
2. Мы гарантируем, что отправка прошла успешно
3. Мы вызываем метод получения контракта и проверяем, что вызов прошел успешно.
4. Мы сравниваем результаты, полученные от метода получения, с адресом from , который мы установили в исходном внутреннем сообщении.
Кажется довольно простым и выполнимым! Давай сделаем это.
Команда Sandbox предоставила нам еще одну замечательную утилиту для тестирования. Мы можем установить дополнительный пакет @ton-community/test-utils, запустив yarn add @ton-community/test-utils -D
Это позволит нам использовать .toHaveTransaction для средства сопоставления jest, чтобы добавить дополнительных помощников для упрощения тестирования. Нам также нужно будет импортировать этот пакет в наш test/main.spec.ts после установки.
Давайте посмотрим, как выглядит наш тестовый код, основанный на описанном выше сценарии.
import { Cell, toNano } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";
import { MainContract } from "../wrappers/MainContract";
import "@ton-community/test-utils";
describe("main.fc contract tests", () => {
it("should get the proper most recent sender address", async () => {
const blockchain = await Blockchain.create();
const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0];
const myContract = blockchain.openContract(
await MainContract.createFromConfig({}, codeCell)
);
const senderWallet = await blockchain.treasury("sender");
const sentMessageResult = await myContract.sendInternalMessage(
senderWallet.getSender(),
toNano("0.05")
);
expect(sentMessageResult.transactions).toHaveTransaction({
from: senderWallet.address,
to: myContract.address,
success: true,
});
const data = await myContract.getData();
expect(data.recent_sender.toString()).toBe(senderWallet.address.toString());
});
});
Мы используем функциональность Jest, чтобы убедиться, что:
- отправка внутреннего сообщения прошла успешно
- метод получения прошел успешно
- метод получения возвращает некоторый результат
- адрес, возвращаемый методом получения, равен тому, который мы использовали в нашем сообщении, устанавливая адрес отправителя
Мы также переименовали «наш первый тест» в «должен получить правильный последний адрес отправителя», мы хотим, чтобы имена наших тестов всегда были читабельными.
Запуск тестов
Вуаля! Мы готовы провести наши тесты. Просто запустите нашу команду yarn testв терминале, и если вы все сделали вместе со мной, вы получите аналогичный результат:
PASS tests/main.spec.ts
main.fc contract tests
✓ should get the proper most recent sender address (444 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.13 s, estimated 5 s
И последнее, что мы хотим сделать, это убедиться, что каждый раз, когда мы запускаем тесты, мы также запускаем наш сценарий компилятора. Это полезно для эффективности нашей работы. Большая часть работы по разработке заключается в написании функционального кода и последующем запуске тестов. Давайте упростим это:
Обновите файл package.json , чтобы он выглядел следующим образом:
{
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn compile && yarn jest"
}
}
На следующем уроке мы собираемся построить наш конвейер развертывания и узнать, как протестировать реальный контракт развертывания в сети.
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn compile && yarn jest"
}
}
На следующем уроке мы собираемся построить наш конвейер развертывания и узнать, как протестировать реальный контракт развертывания в сети.
3.5 Развертывание смарт-контракта.pdf
490.4 KB
📚Конспекты лекций на русском
3.5 Развертывание смарт-контракта
3.5 Развертывание смарт-контракта
👎3👍1
3_6_Гибкое_развертывание_из_тестовой_сети_в_основную.pdf
155.4 KB
📚Конспекты лекций на Русском
3.6 Гибкое развертывание из тестовой сети в основную
3.6 Гибкое развертывание из тестовой сети в основную
3_4_Рабочий_процесс_тестирования_и_написание_тестов.pdf
456.5 KB
📚Конспекты лекций
3.4 Рабочий процесс тестирования и написание тестов
3.4 Рабочий процесс тестирования и написание тестов
3_3_Написание_простого_функционального_контракта.pdf
218.2 KB
📚Конспекты лекций
3.3 Написание простого функционального контракта
3.3 Написание простого функционального контракта
3_2_Настройка_рабочего_процесса_компиляции.pdf
270.6 KB
📚Конспекты лекций
3.2 Настройка рабочего процесса компиляции
3.2 Настройка рабочего процесса компиляции
3_1_Обзор_жизненного_цикла_разработки_смарт_контрактов.pdf
213 KB
📚Конспекты лекций
3.1 Обзор жизненного цикла разработки смарт-контрактов
3.1 Обзор жизненного цикла разработки смарт-контрактов
4.1 контракт со встречной логикой_.pdf
175.4 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
4_2_тесты_для_встречного_контракта.pdf
211.1 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
4_3_Контракт_с_логикой_вводавывода_средств.pdf
331.7 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👎2
4_4_Тесты_на_логику_пополнения_вывода_средств.pdf
256.5 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
4_5_использование_Blueprint_для_развертывания_контрактов_.pdf
267.5 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👎1
4_6_проверка_исходного_кода_контракта_контрактов_.pdf
400.7 KB
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1