Рассмотрим простейший проект счётчика. Функций у счётчика будет две - увеличить его на произвольное число и получить текущее значение счётчика. Для простоты реализации мы не будем добавлять функцию сброса и будем увеличивать значение через dApp только на 1. То есть реализуем инкремент для счётчика и получение результата после этого действия.
Этапы разработки проекта
Так как мы разрабатываем dApp для TON блокчейна, то нам нужно:
Написать смарт контракт для нашего счётчика на языке программирования FunC
Протестировать его выполнение, так как наш смарт контракт нельзя будет изменить после его развертывания в TON блокчейн
Создать интерфейс приложения
Связать этот интерфейс с TON блокчейн
Протестировать работу dApp - получить текущее значение счётчика, выполнить инкремент через dApp и получить обновленный результат. Он должен быть равен предыдущему значению увеличенному на 1.
Создание смарт контракта
Для простоты понимания рассмотрим структуру готового репозитория с кодом, который можно использовать как шаблон для собственных dApp Demo dApp for TON blockchain that can be used as boilerplate
В корне репозитория расположена папка contracts, в которой расположены папки src и build. Src используется для исходного кода смарт контрактов, а build содержит информацию для разворачивания на TON блокчейне и дебага.
Исходный код смарт контракта счетчика находится по ссылке.
Разберем этот код
() recv_internal(slice in_msg) impure { ;; точка входа или главная функция, которая принимает все внутренние сообщения, impure говорит, что функция изменяет состояние контракта
int n = in_msg.preload_uint(32); ;; загружаем число, на которое нужно увеличить счетчик из сообщения
slice ds = get_data().begin_parse(); ;; получаем данные из регистра c4, то есть данные сохраненные в смарт контракте
int total = 0; ;;помечаем, что если хранилище пустое, то счетчик равен 0
if (ds.slice_empty?() == 0) { ;; не пустое хранилище
total = ds.preload_uint(64); ;; загружаем текущее значение счётчика
}
set_data(begin_cell().store_uint(n + total, 64).end_cell()); ;; сохраняем сумму полученного числа и текущего счётчика в хранилище
}
int get_total() method_id { ;; get метод, при вызове которого пользователь может получить текущее значение счётчика
slice ds = get_data().begin_parse(); ;; получаем данные из регистра c4, то есть данные сохраненные в смарт контракте
int total = 0; ;;помечаем, что если хранилище пустое, то счетчик равен 0
if (ds.slice_empty?() == 0) { ;; не пустое хранилище
total = ds.preload_uint(64); ;; загружаем текущее значение счётчика
}
return total; ;; отдаём пользователю текущее значение счётчика
}
Есть и другой вариант смарт контракта, когда вместе с развертыванием, мы инициализируем данные контракта, в таком случае код упрощается, потому что не нужно проверять, пустое ли хранилище и мы можем просто складывать полученное значение с текущим значением счетчика. Данный вариант рассмотрен для наглядности и не претендует на роль использования в реальном проекте. Развёртывание смарт контрактов будет рассмотрено ниже.
Тестирование смарт контрактов
Тестирование смарт контрактов является обязательной частью их разработки, так как зачастую разработчик не сможет исправить логику, заложенную в нём. Существуют несколько подходов для тестирования, мы же рассмотрим один из них, при помощи пакета npm. Данные способ имеет несомненное приемущество, по сравнению с остальными, так как в данном случае нам не нужен блокчейн вообще. Все тесты можно выполнять локально, даже без доступа к сети Интернет.
Все возможности пакета можно найти в его документации. Мы же воспользуемся несколькими приёмами из обширных возможностей этого инструмента.
Рассмотрим тест, который использует jest и ton-contract-executor. Исходный код расположен здесь.
Часть кода теста приведена ниже с комментариями:
const source = fs.readFileSync("./contracts/src/main.fc", {encoding:'utf8', flag:'r'}); // загружаем контракт из файловой системы для работы
let contract = await smcFromSource(source, new Cell(), {
getMethodsMutate: true,
debug: true // enable debug
}); // эта функция определена в файле выше и подключает стандартную библиотеку в каждый смарт контракт, new Cell() означает пустую клетку с данными. Как было изложено выше возможен смарт контракт в котором изначально счетчик устанавливается в 0. Соответственно в этом случае этот аргумент должен содержать клетку beginCell().storeUint(0, 64).endCell()
contract.setBalance(new BN(500)); // устанавливаем баланс контракта
let msgBody = beginCell()
//increment
.storeUint(1, 32)
.endCell(); // подготавливаем сообщение, которое мы отправим в смарт контракт, в данном случае мы просто отправим 1
console.log(msgBody.toBoc().toString('base64')); // печатаем boc в кодировке base64, такой прием удобен для создания дальнейшего интерфейса пользователя для работы со смарт контрактом. В этом случае мы получаем boc, который пользователь может отправить в смарт контракт для выполнения инкремента
const counter = [1,2,3,4,5]; // создаём массив итераций
for (let i in counter) { // инициализируем цикл для отправки сообщений
const res = await contract.sendInternalMessage(new InternalMessage({
to: Address.parse('EQD4FPq-PRDieyQKkizFTRtSDyucUIqrj0v_zXJmqaDp6_0t'), // адрес контракт может быть любым
value: 100, // 10 nanoton // значение которое мы прикрепляем для фазы вычисления
bounce: false, // смарт контракт не вернёт нам обратно средства, если он не инициализирован, в данном случае, он уже будет развернут
body: new CommonMessageInfo({
body: new CellMessage(msgBody) // клетка сообщения, которую мы подготовили выше
})
}));
expect(res.type).toEqual('success'); // проверяем, что фаза вычисления для смарт контракт успешна
let resGetTotal = await contract.invokeGetMethod('get_total', []); // вызываем метод get_total без параметров
expect(resGetTotal.result[0].toNumber()).toEqual(counter[i]); // проверяем, что в каждой из итераций счетчик увеличивается на 1
}
Для запуска тестов выполним в консоле
yarn run test
Когда все тесты успешно пройдены и проверены все логические цепочки для смарт контракта можно переходить к компиляции с последующим развертыванием.
Компиляция кода смарт контракта
Для компиляции кода смарт контракта выполняем в консоле:
yarn run compile
Это команда создаст main.boc и main.fift файлы в папке ./contracts/build/
Эта папка должна содержать также файл main.deploy.js (main это название нашего смарт контракта). Этот файл содержит обязательные параметры для развертывания смарт контракта для TON блокчейна. Он должен содержать две обязательные функции - initData и initMessage. Рассмотрим его:
// return the init Cell of the contract storage (according to load_data() contract method)
export function initData() {
return new Cell(); // пустая клетка для c4 регистра, таким образом создаём смарт контракт с пустым хранилищем данных
}
// return the op that should be sent to the contract on deployment, can be "null" to send an empty message
export function initMessage() {
return null; // Сообщение, которое будет отправлено после развёртывания смарт контракта. Может вызывать какую-то функцию смарт контракта для его инициализации.
}
// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain
export async function postDeployTest(walletContract, secretKey, contractAddress) {
const call = await walletContract.client.callGetMethod(contractAddress, "get_total");
const counter = new TupleSlice(call.stack).readBigNumber();
console.log(` # Getter 'get_total' = ${counter.toString()}`); // тестируем, что счетчик возвращает 0 после развертывания
}
Развертывание смарт контракта
Для процесса развертывания нам необходим кошелёк с которого мы будем разворачивать смарт контракт. Информация о таком кошельке берётся из файла .env. Перед процессом развертывания можно переназвать файл .env_example в .env, иначе будет создан новый .env с уже подготовленным новым кошельком, с пустым балансом, который будет необходимо пополнить.
Для развертывания используются две переменные
DEPLOYER_WALLET="org.ton.wallets.v3.r2"
DEPLOYER_MNEMONIC="auto moto bike leg"
Где переменная DEPLOYER_WALLET статичная, но по мере развития может использоваться другая версия кошелька. Переменная DEPLOYER_MNEMONIC является сид-фразов кошелька.
Для развертывания смарт контракта на тестнет TON блокчейна выполним:
yarn run deploy:testnet
Если всё прошло успешно, то приступаем к написанию интерфейса dApp.
Для развертывания смарт контракта main на майннет TON блокчейна необходимо выполнить:
yarn run deploy
Делать это необходимо только в полной уверенности, что код смарт контракта удовлетворяет всем необходимым условиям.
Создание dApp для смарт контракта
Децентрализованные приложение dApp должно использовать провайдер кошелька. Это может быть как веб-расширение браузера, так и мобильное приложение с встроенным браузером. Унифицированный интерфейс для таких приложений описан в стандарте TEPs105 (https://github.com/ton-blockchain/TEPs/pull/105). Этот стандарт определяет как dApp может взаимодействовать с TON блокчейн через провайдер кошелька.
Практически любому dApp будет необходимо работать со следующим списком задач:
Запросить разрешения у пользователя
Получить адрес пользователя и сеть, к которой он подключен. Отслеживать изменения этих параметров
Получать информация из TON блокчейна
Отправлять транзакции в TON блокчейн
Отслеживать состояние выполнения транзакции
Для проверки того, что окружение пользователя поддерживает TEPs105 стандарт можно использовать следующий код:
typeof(window.ton) != "undefined" && window.ton.isTEPs105
Обработка типичных состояний при разработке dApp
Ниже предложен список из типичных состояний пользовательского окружения, которые должно обрабатывать практически любое dApp:
-
Не установлен провайдер кошелька или стандарт TEPs105 (его расширение) не поддерживается после проверки пользовательского окружения.
Необходимо указать пользователю на необходимость установки или использования необходимого окружения для работы с dApp. В данном случае разработчик dApp должен обеспечить работоспособность приложения без провайдера кошелька.
-
У пользователя установлен провайдер кошелька.
dApp может проверить у пользователя разрешения на запрос данных его адреса и текущего сервера подключения. Если такие разрешения были получены, то dApp можно получить эти данные. Если этих разрешений нет, то dApp обязано вызывать метод запроса разрешений для провайдера кошелька.
-
Пользователь сменил текущий адрес кошелька или текущий сервер подключения
dApp должно подписываться на события смены этих данных и реагировать соответствующим образом.
-
Пользователь отправил транзакцию.
dApp должно подписаться на событие получения сообщения о выполнении транзакции на TON блокчейн.
Интерфейс dApp счетчика
Так как приложение достаточно простое, ограничимся полем для вывода текущего значения счетчика и кнопки для инкремента. Кроме того нам нужна кнопка подключения провайдера кошелька. По нажатию на эту кнопку, пользователь должен разрешить dApp запрашивать данные о его аккаунте и сервере текущего подключения. Если пользователь не разрешает получение таких данных, мы объясняем почему они нужны для работы dApp.
Рассмотрим часть функции для инкремента:
const result = await checkPermission(['ton_sendRawTransaction', 'ton_subscribe', 'ton_unsubscribe']); // общая функция для получения разрешения от пользователя
if (result) { // пользователь выдал все необходимы разрешения
let ton_sendRawTransaction;
try {
ton_sendRawTransaction = await window.ton
.request({
method: "ton_sendRawTransaction", // название метода из стандарта TEPs105 для выполнения транзакции
params: {
"to": smartContractAddress, // мы отправляем на адрес нашего смарт контракта
"amount": 0.01, // сумма, необходимая для фазы вычисления
//beginCell().storeUint(1, 32).endCell()
"data": "te6ccsEBAQEABgAGAAgAAAABOYxhGg==", // значение boc, которое мы получили во время тестирования
"dataType": "boc", // тип данных, в данном случае нам необходим boc
"stateInit": "" // этот параметр необходим для развертывания смарт контракта, поэтому мы оставляем его пустым
}
});
if (ton_sendRawTransaction) { // провайдер кошелька отправил транзакцию
show_modal("Wait for the transaction to be confirmed on the TON blockchain."); // показываем пользователю, что всё хорошо и необходимо подождать подтверждения на блокчейне
await subscribeOnTxConfirmation(currentAccount.address); // эта функция подписывается на сообщение о подтверждении транзакции, когда провайдер кошелька получит сообщение на сети блокчейн, он перешлёт его подписчикам
show_modal("Your transaction is confirmed on TON blockchain!"); // показываем пользователю, что всё прошло успешно
update_get_total(currentEndpoint); // обновляем счетчик, запрашивая информацию из блокчейна
}
} catch(e) { // обработка исключений
show_modal(e.message);
update_get_total(currentEndpoint);
}
} else {
show_modal("We need some action from your side to provide our service. Please confirm required permissions."); // объясняем пользователю, что без его разрешения мы не можем выполнить сценарий
}
Выводы
Мы рассмотрели все этапы создания простого dApp на TON блокчейн. Код из репозитория может быть легко использован как шаблон для собственных решений. Разработчики могут добавить знакомые им фреймворки в код и создавать любые по сложности интерфейсы для dApp, а встроенные инструменты для разработки смарт контрактов и готовая структура с тестами поможет комфортно создавать приложения разных уровней сложности.