Как сделать авторизацию блокчейне TON на DART с помощью кошелька через TON Connect

—

от автора

Вступление

В данной статей будет показано, как использовать библиотеку 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 . Буду рад вашей подписке.


ссылка на оригинал статьи https://habr.com/ru/articles/742036/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *