Вступление

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

Комментарии (1)


  1. PackRuble
    15.06.2023 17:28

    Здравствуйте. Спасибо за материал! Было бы неплохо добавить красок коду, указав язык для сниппетов - dart, а также убрать дублирование кода в разделе "Перейдем к логике авторизации"

    Возможно, стоит привязать `disconnect()` к `dispose()` методу виджета в дальнейшем, хоть это и крайне простой пример приложения)