Вступление
В данной статей будет показано, как использовать библиотеку darttonconnect для быстрого создания авторизации и отправки транзакций для блокчейна TON. Таким образом можно быстро создавать кроссплатформенные приложения на Flutter для блокчейна TON.
Зачем нужен TON Connect?
В современных блокчейн приложениях есть разделение между кошельками и децентрализованными приложениями.
Кошельки предоставляют пользовательский интерфейс для подтверждения транзакций и безопасного хранения криптографических ключей пользователей на их личных устройствах.
Приложения являются пользовательским интерфейсом для смарт-контрактов и не имеют прямого доступа к средствам пользователей. Для осуществления транзакции в смарт-контракт, пользователю необходимо авторизоваться с помощью кошелька и подтверждать все транзакции, инициализированные в приложении в кошельке.
TON Connect это протокол для взаимодействия кошельков и приложений в блокчейне TON. Взаимодействие идет через мост. Кошелек "отправляет" события приложению через SSE.
В TON Connect можно использовать любой интегрированный в протокол кошелек, для простоты в данном туториале мы будем использовать Tonkeeper.
Устанавливаем кошелек и переключаем его в тестовую сеть
Для того, чтобы воспользоваться, нужен кошелек на TON, ссылка на Tonkeeper: https://tonkeeper.com/
Так как нам надо будет тестировать транзакции через Tonkeeper, необходимо будет зайти в Tonkeeper и переключить его на тестовую сеть, для этого:
заходи в настройки и листаем в самый низ до надписи Tonkeeper версии 3.0
жмём 6 раз подряд быстро на иконку Tonkeeper над надписью - откроется меню для разработчиков
выбираем переключиться на тестовую сеть в нем
чтобы получить на кошелек в тестовой сети, тестовый TON, нужно воспользоваться ботом: https://t.me/testgiver_ton_bot
Установка библиотек и создание проекта
Зайдите в директорию, где вы будет разрабатывать приложение и создаем проект командой:
flutter create .
В данном туториале нам понадобиться следующие библиотеки:
qr_flutter для создания QR-кода
darttonconnect для реализации логики авторизации через TON Connect
Установите их командами:
flutter pub add qr_flutter flutter pub add darttonconnect
Соберем каркас одностраничного приложения
Так как данный туториал про авторизации, останавливаться на каркасе одностраничного приложения не будем.
Скопируйте код ниже в файл main.dart :
import 'package:flutter/material.dart'; import 'package:darttonconnect/exceptions.dart'; import 'package:darttonconnect/logger.dart'; import 'package:darttonconnect/ton_connect.dart'; import 'package:qr_flutter/qr_flutter.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( fixedSize: MaterialStateProperty.all(const Size(200, 30)))), primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { }
Перейдем к логике авторизации
Наше одностраничное приложение будет состоять из одной страницы и трех кнопок:
Connect
Disconnect
Send transaction
Каждая из этих кнопок будет вызывать соответствующую функцию:
Connect - создание QR кода, который мы будем сканировать кошельком для авторизации
Disconnect - отключение кошелька от приложения
Send transaction - отправка транзакции
Добавим кнопки и вызовы функций (для минимального дизайна будет использоваться material design библиотека):
import 'package:flutter/material.dart'; import 'package:darttonconnect/exceptions.dart'; import 'package:darttonconnect/logger.dart'; import 'package:darttonconnect/ton_connect.dart'; import 'package:qr_flutter/qr_flutter.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( fixedSize: MaterialStateProperty.all(const Size(200, 30)))), primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: initialConnect, child: const Text('Create initial connect')), const SizedBox(height: 15), ElevatedButton( onPressed: disconnect, child: const Text('Disconnect')), const SizedBox(height: 15), ElevatedButton(onPressed: sendTrx, child: const Text('Sendtxes')), const SizedBox(height: 15), if (universalLink != null) QrImageView( data: universalLink!, version: QrVersions.auto, size: 320, gapless: false, ) ], ), ), )); } }
Манифест
Для авторизации через TONConnect нужен файл манифест, в нем прописывается:
url приложения/сайта он будет использоваться для открытия децентрализованного приложения после нажатия на его значок в кошельке.
Название веб-сайта/приложения
Иконка приложения
Название и иконка нужны, чтобы пользователь в кошельке, понимал к чему подключается.
В нашем случае будем использовать тестовый манифест, расположенный Github gist вот: https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/5f933bd5d24f5979b64ae88421e7849dd144efc1/gistfiletest.txt
Подключение
Первое что, надо сделать это создать connector, через который будет происходить соединение. В коннекторе воспользуемся нашим манифестом.
// Initialize TonConnect. final TonConnect connector = TonConnect( 'https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/43e00b0abc824ef272ac6d0f8083d21456602adf/gistfiletest.txt');
В системе TonConnect можно авторизоваться любым кошельком, который подключен к системе TonConnect, получить список можно вот так:
final List wallets = await connector.getWallets();
Так как это упрощенный туториал, то мы будем пользоваться только Tonkeeper'ом
Создадим источник подключения и сгенерируем ссылку для авторизации.
// Initialize TonConnect. final TonConnect connector = TonConnect( 'https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/43e00b0abc824ef272ac6d0f8083d21456602adf/gistfiletest.txt'); Map<String, String>? walletConnectionSource; String? universalLink; /// Create connection and generate QR code to connect a wallet. void initialConnect() async { const walletConnectionSource = { "universalUrl": 'https://app.tonkeeper.com/ton-connect', "bridgeUrl": 'https://bridge.tonapi.io/bridge' }; final universalLink = await connector.connect(walletConnectionSource); updateQRCode(universalLink); connector.onStatusChange((walletInfo) { logger.i('Произошло изменение подключения'); }); }
Как вы можете видеть внизу кода, в случае изменения соединения, коннектор прокинет информацию о кошельке (То есть когда мы авторизуемся, мы сможем увидеть информацию о кошельке). Это очень удобно при отладке приложений, например, можно логировать каждое изменение.
Чтобы ссылкой можно было удобно воспользоваться с мобильного устройства, добавим QR код:
void updateQRCode(String newData) { setState(() => universalLink = newData); }
Если запустить сейчас приложение, то авторизация уже будет работать, но что если мы авторизовались и вышли с сайта, а потом вернулась, для этого мы можем добавить реконнект:
@override void initState() { // Override default initState method to call restoreConnection // method after screen reloading. super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!connector.connected) { restoreConnection(); } }); } /// Restore connection from memory. void restoreConnection() { connector.restoreConnection(); }
Теперь добавим функцию дисконнекта:
/// Disconnect from current wallet. void disconnect() { if (connector.connected) { connector.disconnect(); } else { logger.i("Сначала коннект, потом дисконект"); } }
При дисконнекте мы проверяем подключен ли пользователь и потом дисконектимся. Осталось сделать отправку траназакции, отправка происходит с помощью sendTrx(). В данном примере сразу за хардкожены все данные, итоговый код:
/// Send transaction with specified data. void sendTrx() async { if (!connector.connected) { logger.i("Сначала коннект, потом дисконект"); } else { const transaction = { "validUntil": 1918097354, "messages": [ { "address": "0:575af9fc97311a11f423a1926e7fa17a93565babfd65fe39d2e58b8ccb38c911", "amount": "20000000", } ] }; try { await connector.sendTransaction(transaction); } catch (e) { if (e is UserRejectsError) { logger.d( 'You rejected the transaction. Please confirm it to send to the blockchain'); } else { logger.d('Unknown error happened $e'); } } } }
Итоговый код
import 'package:flutter/material.dart'; import 'package:darttonconnect/exceptions.dart'; import 'package:darttonconnect/logger.dart'; import 'package:darttonconnect/ton_connect.dart'; import 'package:qr_flutter/qr_flutter.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( fixedSize: MaterialStateProperty.all(const Size(200, 30)))), primarySwatch: Colors.blue, ), home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // Initialize TonConnect. final TonConnect connector = TonConnect( 'https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/43e00b0abc824ef272ac6d0f8083d21456602adf/gistfiletest.txt'); Map<String, String>? walletConnectionSource; String? universalLink; @override void initState() { // Override default initState method to call restoreConnection // method after screen reloading. super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!connector.connected) { restoreConnection(); } }); } /// Create connection and generate QR code to connect a wallet. void initialConnect() async { const walletConnectionSource = { "universalUrl": 'https://app.tonkeeper.com/ton-connect', "bridgeUrl": 'https://bridge.tonapi.io/bridge' }; final universalLink = await connector.connect(walletConnectionSource); updateQRCode(universalLink); connector.onStatusChange((walletInfo) { logger.i('Произошло изменение подключения'); }); } /// Restore connection from memory. void restoreConnection() { connector.restoreConnection(); } void updateQRCode(String newData) { setState(() => universalLink = newData); } /// Disconnect from current wallet. void disconnect() { if (connector.connected) { connector.disconnect(); } else { logger.i("Сначала коннект, потом дисконект"); } } /// Send transaction with specified data. void sendTrx() async { if (!connector.connected) { logger.i("Сначала коннект, потом дисконект"); } else { const transaction = { "validUntil": 1918097354, "messages": [ { "address": "0:575af9fc97311a11f423a1926e7fa17a93565babfd65fe39d2e58b8ccb38c911", "amount": "20000000", } ] }; try { await connector.sendTransaction(transaction); } catch (e) { if (e is UserRejectsError) { logger.d( 'You rejected the transaction. Please confirm it to send to the blockchain'); } else { logger.d('Unknown error happened $e'); } } } } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: initialConnect, child: const Text('Create initial connect')), const SizedBox(height: 15), ElevatedButton( onPressed: disconnect, child: const Text('Disconnect')), const SizedBox(height: 15), ElevatedButton(onPressed: sendTrx, child: const Text('Sendtxes')), const SizedBox(height: 15), if (universalLink != null) QrImageView( data: universalLink!, version: QrVersions.auto, size: 320, gapless: false, ) ], ), ), )); } }
Заключение
Спасибо за внимание, ссылка на пример из статьи, ссылка на библиотеку. Подобные технические статьи я пишу в https://t.me/ton_learn . Буду рад вашей подписке.
