Вступление
В данной статей будет показано, как использовать библиотеку 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 . Буду рад вашей подписке.
PackRuble
Здравствуйте. Спасибо за материал! Было бы неплохо добавить красок коду, указав язык для сниппетов - dart, а также убрать дублирование кода в разделе "Перейдем к логике авторизации"
Возможно, стоит привязать `disconnect()` к `dispose()` методу виджета в дальнейшем, хоть это и крайне простой пример приложения)