TON Blockchain Сourse #FREEDUROV
737 subscribers
17 photos
73 videos
30 files
127 links
для связи @Aleksey_Leon

#freedurov
Download Telegram
Пишем наш скрипт компиляции
Создаем папку scripts в корне нашего проекта и новый файл compile.ts в папке scripts:

mkdir scripts && cd scripts && touch compile.ts

У нас есть файл compile.ts, но давайте сделаем так, чтобы его было удобно запускать, как только мы закончим с его кодом. Давайте создадим ярлык скрипта в файле package.json. Просто добавьте следующий ключ scripts:

{
//...your previous package.json contents
"scripts": {
"compile": "ts-node ./scripts/compile.ts"
}
}


Теперь откройте файл scripts/compile.ts в вашем редакторе и давайте начнем писать наш скрипт компиляции. Мы собираемся добавлять код шаг за шагом вместе с пояснениями, так что к концу этого раздела у вас будет полный код для скрипта компиляции.

Прежде всего, мы импортируем:

fs для работы с файлами
process для управления процессом выполнения скрипта
Конструктор Cell (байт-код нашего контракта будет сохранен как Cell)
compileFunc - фактическая функция компиляции

import * as fs from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";


У нас будет некоторый асинхронный код, поэтому давайте создадим асинхронную функцию, которая будет запускать его внутри:

import * as fs from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";

async function compileScript() {

}

compileScript();


Теперь давайте передадим имя файла нашего образца контракта в функцию compile Func и завершим скрипт, если статус результата - ошибка:

import * as fs from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";

async function compileScript() {

const compileResult = await compileFunc({
targets: ["./contracts/main.fc"],
sources: (x) => fs.readFileSync(x).toString("utf8"),
});

if (compileResult.status === "error") {
process.exit(1);
}

}
compileScript();



Классно! Технически, вы уже могли бы перейти в корень нашего проекта и запустить команду yarn compile. Но если вы это сделаете - вы не сможете увидеть результаты компиляции. Давайте сохраним результаты нашей компиляции в папке build в корне нашего проекта (убедитесь, что вы сначала создали ее).

Результатом компиляции будет строка BOC (Body of Cell) base64, но если мы хотим в дальнейшем работать с этим скомпилированным контрактом локально (при написании тестов), нам нужно создать ячейку, которая будет содержать этот BOC, и сохранить ее для последующего использования.

У конструктора ячейки есть метод .fromBoc, который ожидает получения буфера, поэтому мы предоставим ему буфер из строки BOC base64. Как только мы создадим ячейку из BOC - мы хотим создать шестнадцатеричное представление этой ячейки и сохранить его в файле JSON. Вот как выглядит команда построения и преобразования ячейки:

Cell.fromBoc(Buffer.from(compileResult.codeBoc, "base64"))[0].toBoc() .toString("hex")

А теперь давайте поместим это в наш скрипт, а также используем fs.writeFileSync для сохранения результатов в наш JSON-файл:

import * as fs from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";

async function compileScript() {

const compileResult = await compileFunc({
targets: ["./contracts/main.fc"],
sources: (x) => fs.readFileSync(x).toString("utf8"),
});

if (compileResult.status === "error") {
process.exit(1);
}

const hexArtifact = `build/main.compiled.json`;

fs.writeFileSync(
hexArtifact,
JSON.stringify({
hex: Cell.fromBoc(Buffer.from(compileResult.codeBoc, "base64"))[0]
.toBoc()
.toString("hex"),
})
);
}
compileScript();


Этот код готов к запуску, но давайте внесем некоторые окончательные правки, чтобы сделать наш скрипт информативным во время его выполнения. Мы собираемся добавлять красивые консольные журналы на каждом шаге.

Вот окончательный код вместе с консольными журналами:
import * as fs from "fs";
import process from "process";
import { Cell } from "ton-core";
import { compileFunc } from "@ton-community/func-js";

async function compileScript() {
console.log(
"================================================================="
);
console.log(
"Compile script is running, let's find some FunC code to compile..."
);

const compileResult = await compileFunc({
targets: ["./contracts/main.fc"],
sources: (x) => fs.readFileSync(x).toString("utf8"),
});

if (compileResult.status === "error") {
console.log(" - OH NO! Compilation Errors! The compiler output was:");
console.log(`\n${compileResult.message}`);
process.exit(1);
}

console.log(" - Compilation successful!");

const hexArtifact = `build/main.compiled.json`;

fs.writeFileSync(
hexArtifact,
JSON.stringify({
hex: Cell.fromBoc(Buffer.from(compileResult.codeBoc, "base64"))[0]
.toBoc()
.toString("hex"),
})
);

console.log(" - Compiled code saved to " + hexArtifact);
}
compileScript();


Это наш скрипт компиляции, который мы будем запускать каждый раз, когда будем вносить какие-либо изменения в наш код. Позже, когда у нас будет некоторая тестируемая функциональная логика и сценарии развертывания - мы сможем автоматизировать запуск этого скрипта перед любыми тестами или развертыванием.

Наша лаборатория "satellite assembly" оснащается все больше и больше!

Но давайте еще раз запустим yarn compile и посмотрим, каковы результаты нашей компиляции. Для этого давайте откроем наш основной файл.compiled.fc.

Что мы там видим:

{"hex":"b5ee9c72410102010012000114ff00f4a413f4bcf2c80b010006d35f03fbffbf07"}

Звучит безумно, но это наш контракт. На следующем уроке мы собираемся написать нашу первую функциональную логику и скомпилировать ее с помощью только что написанного сценария.
🔥1
Media is too big
VIEW IN TELEGRAM
3.3 Написание простого функционального контракта
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5👨‍💻1
📚Конспекты лекции
На предыдущем уроке мы создали скрипт компиляции, и теперь пришло время приступить к написанию функционального кода. Знаете, что круто? Теперь мы готовы проверить, работает ли наш функциональный код на самом деле. Как только мы напишем какой-нибудь навороченный код - мы просто запустим yarn compile и убедимся, что наш код может работать на TVM.

Смарт-контракт в этом уроке будет очень-очень простым, но нам этого будет достаточно, чтобы ознакомиться с базовыми функциями sintax и структурой.
Параметры, которые мы получаем с помощью внутреннего обработчика сообщений recv_internal
Давайте разберемся с кодом. В предыдущем уроке мы уже создали файл contracts/main.fc. Давайте откроем его и посмотрим, что у нас там есть:

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

}


Что у нас уже есть, так это функция, которая обрабатывает входящие сообщения. Как вы уже знаете из главы 1, любые транзакции в TON называются messages.

Итак, что нам приносит сообщение? Что мы уже можем видеть, так это три параметра, которые передаются в функцию recv_internal:

msg_value - этот параметр сообщает нам, сколько тонн монет (или граммов) получено с этим сообщением
in_msg - это полное сообщение, которое мы получили, со всей информацией о том, кто его отправил и т.д. Мы видим, что у него есть тип Cell. Что это значит? Тело сообщения хранится в виде ячейки в TVM, поэтому для нашего сообщения выделена целая ячейка со всеми его данными.
in_msg_body - это фактическая "читаемая" часть сообщения, которое мы получили. Он имеет тип slice, поскольку он является частью ячейки, он указывает "адрес", с какой части ячейки мы должны начать чтение, если мы хотим прочитать этот параметр slice.
TODO: Подробнее о содержимом ячейки in_msg.

Как вы можете видеть, и msg_value, и in_msg_body являются производными от in_msg, но для удобства использования мы получаем их в качестве параметров в функцию receive_internal.

Спецификаторы функций
Я уверен, что вы заметили слово impure сразу после параметров, переданных в функцию. Это один из 3 возможных спецификаторов функций:

impure
inline/inline_ref
method_id

Один, несколько или ни один из них не может быть помещен в объявление функции, но в настоящее время они должны быть представлены в правильном порядке. Например, не разрешается помещать impure после inline.

На данный момент нас интересует только спецификатор impure, но мы собираемся рассмотреть остальные, как только они начнут появляться в нашем коде.

impure спецификатор означает, что функция может иметь некоторые побочные эффекты, которые нельзя игнорировать. Например, мы должны указать нечистый спецификатор, если функция может изменять хранилище контрактов, отправлять сообщения или генерировать исключение, когда некоторые данные недействительны, и функция предназначена для проверки этих данных.

Если значение impure не указано и результат вызова функции не используется, то компилятор FunC может удалить и удалит этот вызов функции.

Importing stdlib.fc

Чтобы манипулировать данными и писать другую логику в нашем контракте, нам нужно сделать еще одну важную вещь. Нам нужно импортировать стандартную библиотеку FunC. В настоящее время эта библиотека является просто оболочкой для наиболее распространенного ассемблера команд TVM, которые не являются встроенными. Описание каждой команды TVM, используемой в библиотеке, можно найти в документации.

Чтобы импортировать stdlib.fc, давайте создадим папку imports внутри нашей папки contracts. Затем создайте файл stdlib.fc и заполните его содержимым официальной библиотеки stdlib.fc, которую вы можете получить здесь.

Теперь, в самом начале нашего main.fc нам нужно вставить фактический импорт:

#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {

}


Отлично, теперь мы готовы идти!
Разбор in_msg
Давайте, наконец, узнаем, что мы можем сделать с параметрами, переданными нашей функции recv_internal.

Всякий раз, когда мы хотим обработать внутреннее сообщение, прежде чем мы даже начнем читать содержательную часть in_msg_body, нам нужно сначала понять, какого рода внутреннее сообщение мы получили. Случаи могут быть разными. Например, мы могли бы получить это сообщение, потому что наш контракт ранее отправил кому-то какое-то сообщение, и принимающая сторона не смогла его принять, поэтому оно "отскочило" обратно. Иногда мы не хотим обрабатывать сообщения такого рода. Мы собираемся поговорить о таких сценариях позже в этом курсе.

В каждом сообщении, которое мы получаем, есть флаги. Флаги - это, по сути, 4-разрядное целое число, где каждый бит ... (TODO: Elaborate on structure of flags. int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool)

Эти флаги на самом деле сообщат нам ценную информацию о сообщении, такую как "Это сообщение было отскочено получателем, и теперь оно фактически возвращается обратно".

Итак, первое, что должен сделать наш контракт при получении внутреннего сообщения - проанализировать его. Давайте проанализируем in_msg:

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
}


Давайте разберем, что именно происходит в этом коде:

Вам нужно будет запомнить эту концепцию, этот фрагмент - это "address", указатель. Итак, когда мы анализируем - мы анализируем, начиная с некоторого места. В этом случае функция begin_parse() сообщает нам, с чего мы должны начать синтаксический анализ, она дает нам указатель на самый первый бит ячейки in_msg.

Затем мы анализируем 4-битное целое число, вызывая load_uint(4), и присваиваем результат переменной int flags.

Как только мы собираемся вызвать еще несколько ~load_{*} для переменной cs, мы фактически продолжим синтаксический анализ с того места, где завершился предыдущий ~load_{*}.

В случае, если мы попытаемся проанализировать что-то, чего на самом деле не существует в ячейке - наш контракт завершится с кодом 9. Вы можете прочитать больше о стандартных ошибках кода здесь

В ячейке in_msg есть еще кое-какая ценная информация, а именно - адрес отправителя, поэтому давайте продолжим разбор:

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
}


Когда мы хотим создать переменную, хранящую адрес, мы всегда используем тип slice, поэтому мы сохраняем pointer только оттуда, откуда память должна считывать адрес, как только это потребуется.

Что мы собираемся делать с переменными, которые у нас уже есть? Позвольте мне сначала познакомить вас с еще двумя возможностями смарт-контракта:

Наш смарт-контракт имеет постоянное хранилище (называемое хранилищем c4)
Наш смарт-контракт может иметь метод получения, который позволит любому человеку из-за пределов мира получить некоторые данные из нашего контракта.
Используя эти две новые возможности, мы можем сделать следующее. Мы можем сохранить адрес отправителя в нашем хранилище и создать метод получения, который будет возвращаться при его вызове.

Другими словами, наш получатель всегда возвращал бы адрес контракта, который отправил сообщение нашему контракту последним.
Использование постоянного хранилища
Чтобы сохранить те же данные в нашем постоянном хранилище c4, мы собираемся использовать необычную стандартную функцию set_data. Эта функция принимает и сохраняет ячейку.

Если мы хотим сохранить больше данных, чем помещается в ячейку, мы можем легко записать "ссылку" на другую ячейку внутри первой. Такая ссылка называется ref. Мы можем записать до 4 refs ссылок в ячейку.

Давайте обновим наш код функцией set_data и узнаем, как мы передаем в нее ячейку.

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();

set_data(begin_cell().end_cell());
}


Чтобы передать ячейку в set_data, нам нужно сначала создать ее. Это легко делается с помощью двух функций begin_cell() и end_cell().

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

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();

set_data(begin_cell().store_slice(sender_address).end_cell());
}


Для этой цели мы используем method .store_slice().

Вуаля, у нас есть смарт-контракт, который способен записывать адрес отправителя в постоянное хранилище! Каждый раз, когда наш контракт будет получать внутреннее сообщение, он будет заменять ячейку, хранящуюся в c4, новой ячейкой, в которой будет новый адрес отправителя. Все очень просто.
👍1
Используя методы получения
Как вы помните, мы не хотели просто сохранять адрес отправителя. Мы хотели, чтобы любой мог прочитать последний адрес отправителя. Чтобы получить доступ к таким данным из-за пределов TVM, в нашем контракте должна быть специальная функция.

Недавно мы говорили о спецификаторах функций. Чтобы сделать наши данные доступными за пределами TVM, мы собираемся создать функцию и использовать указанный method_id. Если у функции задан этот спецификатор - тогда ее можно вызвать в lite-клиенте orton-explorer как get-метод по ее имени.

Давайте создадим один:

slice get_the_latest_sender() method_id {

}


Функции-получатели размещаются вне функции recv_internal

Как вы можете видеть, мы определяем, какое время должно быть возвращено этой функцией, имя функции и спецификатор method_id.

Теперь давайте напишем логику считывания данных из постоянного хранилища и возврата их значения. Для этого мы используем стандартную функцию FunC get_data:

slice get_the_latest_sender() method_id {
slice ds = get_data().begin_parse();
return ds~load_msg_addr();
}


Как вы можете видеть, мы снова используем begin_parse(), чтобы получить указатель, из которого мы собираемся проанализировать ячейку, хранящуюся в хранилище c4.

Чтобы загрузить сохраненный адрес, мы используем ~load_msg_addr для загрузки адреса.
Составляем наш контракт
Наш окончательный код выглядит следующим образом:

#include "imports/stdlib.fc";

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
slice cs = in_msg.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();

set_data(begin_cell().store_slice(sender_address).end_cell());
}

slice get_the_latest_sender() method_id {
slice ds = get_data().begin_parse();
return ds~load_msg_addr();
}


Довольно простой контракт, но мы многому научились, пока его кодировали, не так ли?

Поскольку мы написали весь запланированный код - давайте запустим компиляцию lovely yarn в нашем терминале.

Если вы проделали все шаг за шагом вместе со мной, вы должны увидеть следующий результат в терминале:

=================================================================
Compile script is running, let's find some FunC code to compile...
- Compilation successful!
- Compiled code saved to build/main.compiled.json
Done in 1.40s.


Давайте проверим build/main.compiled.json, и мы увидим, что его содержимое изменилось - на этот раз шестнадцатеричное значение намного длиннее :) Это потому, что вы написали свой первый функциональный код! Мои поздравления!
👍1🔥1
📚Конспекты лекции
Мы написали наш первый функциональный контракт, мы знаем, что он успешно компилируется. Что дальше?

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

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

Отлично, но как нам убедиться, что это работает? Что ж, мы могли бы внедрить это в блокчейн (что мы в конечном итоге сделаем очень скоро, на следующем уроке), но у TON есть некоторые инструменты, которые позволяют нам моделировать определенное поведение локально.

Это делается с помощью sandbox. Эта библиотека позволяет вам эмулировать произвольные смарт-контракты TON, отправлять им сообщения и запускать методы get для них, как если бы они были развернуты в реальной сети. Поскольку у нас есть "лаборатория" TypeScript, мы можем создать последовательность тестов с помощью другой библиотеки - jest. Таким образом, у нас есть набор тестов, который имитирует все важные поведения с различными входными данными и проверяет результаты. Это лучший подход к написанию, отладке и полному тестированию ваших контрактов перед запуском их в сеть.
Media is too big
VIEW IN TELEGRAM
3.4 Рабочий процесс тестирования и написание тестов
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👨‍💻1
Готовим наш набор тестов
Прежде всего, предполагая, что вы в данный момент находитесь в корне нашего проекта, давайте установим sandbox, jest и еще одну библиотеку, которая нам понадобится для взаимодействия с TON объектами - ton:

yarn add @ton-community/sandbox jest ts-jest @types/jest ton --dev

Нам также нужно будет создать jest.config.js файл в корне нашего проекта для just со следующим содержимым:

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};


Теперь давайте создадим новую папку test с файлом main.spec.ts:

mkdir tests && cd tests && touch main.spec.ts

Давайте настроим наш основной файл.spec.ts для написания наших первых тестов:

describe("main.fc contract tests", () => {

it("our first test", async () => {

});

});


Если вы никогда не писали тесты на TypeScript, вам обязательно стоит попробовать этот подход. При программировании НА смарт-контрактах вы потратите более половины своего времени на написание тестов. Помните наш пример с космическим спутником? То же самое и здесь, мы моделируем каждый важный случай, прежде чем развертывать контракт даже для тестирования net.

Теперь попробуйте запустить команду yarn jest в корне вашего каталога. Если вы все установили правильно (шаг за шагом со мной) - вы должны увидеть следующее:

PASS tests/main.spec.ts
main.fc contract tests
✓ our first test (1 ms)


Отлично, давайте немедленно создадим еще один ярлык для запуска скрипта в нашем файле package.json:

{
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn jest"
}
}
Создание экземпляра контракта
Чтобы написать наш первый тест, нам нужно понять, как мы можем создать экземпляр TypeScript нашего скомпилированного контракта с помощью sandbox.

Ранее мы обсуждали, что код нашего контракта сохраняется в виде ячейки после компиляции. В нашем файле build/main.compiled.json у нас есть шестнадцатеричное представление нашей ячейки. Давайте импортируем его в наш файл с тестами вместе с типом ячейки из to-core:

import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";

describe("main.fc contract tests", () => {

it("our first test", async () => {

});

});


Теперь, чтобы восстановить наш шестнадцатеричный код и получить реальную ячейку, мы воспользуемся этой командой:

const codeCell = Cell.fromBoc(Buffer.from(hex, "hex"))[0]

Мы создаем буфер из шестнадцатеричной строки и передаем его в .fromBocc.

Теперь давайте взглянем на краткое руководство по запуску sandbox. Мы видим, что перед получением экземпляра контракта мы должны получить экземпляр блокчейна.

Давайте импортируем класс Blockchain из библиотеки sandbox и вызовем его метод .create().

import { Cell } from "ton-core";
import { hex } from "../build/main.compiled.json";
import { Blockchain } from "@ton-community/sandbox";

describe("main.fc contract tests", () => {
it("our first test", async () => {
const blockchain = await Blockchain.create();

});
});


Теперь давайте подготовимся к созданию экземпляра contract для взаимодействия. В документации Sandbox указано, что рекомендуемый способ его использования - написать оболочки для вашего контракта, используя интерфейс Contract из ton-core

Что бы это значило для нас? Давайте создадим новую папку wrappers и файл с именем Main Contract.ts внутри нее. Этот файл будет реализовывать и экспортировать оболочку вокруг нашего контракта.

mkdir wrappers && cd wrappers && touch MainContract.ts

Убедитесь, что вы запустили эту последовательность команд из корневого каталога вашего проекта

Откройте MainContract.ts для редактирования. Давайте импортируем интерфейс Contract из библиотеки to-core, затем определим и экспортируем класс, который будет реализовывать Contract.

import { Contract } from 'ton-core';

export class MainContract implements Contract {

}


Если вы взглянете на содержимое интерфейса Contract - вы увидите, что он ожидает параметры address, init и abi. Мы будем использовать только address и init для наших целей. Чтобы использовать их, мы определяем конструктор для нашего класса MainContract

Если вы понятия не имеете, о чем мы здесь говорим с классами и конструкторами - возможно, вам стоит почитать больше об объектно-ориентированном программировании. FunC этого не требует, но для лучшего тестирования с помощью TypeScript вам не следует использовать эти базовые понятия.

import { Address, Cell, Contract } from "ton-core";

export class MainContract implements Contract {
constructor(
readonly address: Address,
readonly init?: { code: Cell; data: Cell }
) {}
}


Свойство init очень интересно. В нем указано начальное состояние нашего контракта. Code - это, очевидно, код контракта. Данные - это еще интереснее. Помните, мы говорили о постоянном хранилище c4 нашего контракта? С помощью этой ячейки данных мы можем определить, что будет находиться в этом хранилище после первого выполнения нашего контракта. Оба этих значения Cell имеют тип ячейки, поскольку они хранятся в памяти TVM в течение жизненного цикла контракта. Тот же код и ячейки данных также используются для расчета будущего адреса нашего контракта.

Обратите внимание, что мы также импортировали классы Address и Cell из библиотеки to-core.

Теперь давайте определим статический метод для нашего MainContract класса - createFromConfig. Сейчас мы не собираемся использовать какие-либо параметры конфигурации, но в будущем мы предполагаем, что нам понадобятся входные данные для создания экземпляра контракта.
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)
);
});
});


На данный момент у нас есть экземпляр смарт-контракта, с которым мы можем взаимодействовать во многом аналогично реальному контракту, чтобы протестировать ожидаемое поведение.
Взаимодействие с контрактом
Библиотека 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 , чтобы он выглядел следующим образом:
{
... our previous package.json keys
"scripts": {
... previous scripts keys
"test": "yarn compile && yarn jest"
}
}

На следующем уроке мы собираемся построить наш конвейер развертывания и узнать, как протестировать реальный контракт развертывания в сети.
Media is too big
VIEW IN TELEGRAM
3.5 Развертывание смарт-контракта
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1👨‍💻1
3.5 Развертывание смарт-контракта.pdf
490.4 KB
📚Конспекты лекций на русском
3.5 Развертывание смарт-контракта
👎3👍1
Media is too big
VIEW IN TELEGRAM
3.6 Гибкое развертывание из тестовой сети в основную
✔️ Так же смотрите видео на YouTube
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻11
3_6_Гибкое_развертывание_из_тестовой_сети_в_основную.pdf
155.4 KB
📚Конспекты лекций на Русском
3.6 Гибкое развертывание из тестовой сети в основную