Introduction
This article will show you how to use the darttonconnect library to quickly create an authorization and send transactions for the TON blockchain. Thus, you can quickly create cross-platform applications on Flutter for the TON blockchain.
Why do we need TON Connect?
In modern blockchain applications, there is a separation between wallets and decentralized applications.
Wallets provide a user interface for confirming transactions and securely storing users' cryptographic keys on their personal devices.
Applications are the user interface for smart contracts and do not have direct access to user funds. To carry out a transaction in a smart contract, the user needs to log in using the wallet and confirm all transactions initiated in the application in the wallet.
TON Connect is a protocol for the interaction of wallets and applications on the TON blockchain. The interaction goes through the bridge. The wallet "sends" events to the application via SSE.
You can use any wallet integrated into the protocol in TON Connect, for simplicity in this tutorial we will use Tonkeeper.
Install the wallet and switch it to the test network
In order to use it, you need a TON wallet, link to Tonkeeper: https://tonkeeper.com/
Since we will need to test transactions through Tonkeeper, it will be necessary to go to Tonkeeper and switch it to the test network, for this:
- go to the settings and scroll down to the bottom to the inscription Tonkeeper version 3.0
- click 6 times in a row quickly on the Tonkeeper icon above the inscription - a menu for developers will open
- choose to switch to the test network in it
- to get a test TON to your wallet on the test network, you need to use the bot: https://t.me/testgiver_ton_bot
Install libraries and create a project
Go to the directory where you will develop the application and create a project with the command:
flutter create .
In this tutorial, we will need the following libraries:
- qr_flutter to generate QR code
- darttonconnect to implement authorization logic through TON Connect
Install them with the commands:
flutter pub add qr_flutter
flutter pub add darttonconnect
Let's assemble the skeleton of a single-page application
Since this tutorial is about authorization, we will not dwell on the framework of a single-page application.
Copy the code below into the main.dart
file:
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> {
}
Let's move on to the authorization logic
Our single page application will consist of one page and three buttons:
- connect
- Disconnect
- Send transaction
Each of these buttons will call the corresponding function:
- Connect - creating a QR code that we will scan with a wallet for authorization
- Disconnect - disconnecting the wallet from the application
- Send transaction - send a transaction
Let's add buttons and function calls (for a minimal design, the material design library will be used):
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,
)
],
),
),
));
}
}
Manifest
For authorization through TONConnect, you need a manifest file, it says:
- app/site url it will be used to open the decentralized application after clicking on its icon in the wallet.
- Website/App name
- Application icon
The name and icon are needed so that the user in the wallet understands what he is connecting to.
In our case, we will use the test manifest located on the Github gist here: https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/5f933bd5d24f5979b64ae88421e7849dd144efc1/gistfiletest.txt
Connection
The first thing to do is to create a connector
through which the connection will occur. In the connector, we will use our manifest.
// Initialize TonConnect.
final TonConnect connector = TonConnect( 'https://gist.githubusercontent.com/romanovichim/e81d599a6f3798bb9f74ab1970a8b376/raw/43e00b0abc824ef272ac6d0f8083d21456602adf/gistfiletest.txt');
In the TON Connect system, you can log in with any wallet that is connected to the TonConnect system, you can get the list like this:
final List wallets = await connector.getWallets();
Since this is a simplified tutorial, we will only use Tonkeeper
Let's create a connection source and generate a link for authorization.
// 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('ΠΡΠΎΠΈΠ·ΠΎΡΠ»ΠΎ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΡ');
});
}
As you can see at the bottom of the code, if the connection changes, the connector will send the wallet information (meaning when we log in, we can see the wallet information). This is very convenient when debugging applications, for example, you can log every change.
To make the link convenient to use from a mobile device, add a QR code:
void updateQRCode(String newData) {
setState(() => universalLink = newData);
}
If we start the application now, then authorization will already work, but what if we logged in and left the site, and then returned, for this we can add a reconnect:
@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();
}
Now let's add the disconnect function:
/// Disconnect from current wallet.
void disconnect() {
if (connector.connected) {
connector.disconnect();
} else {
logger.i("Π‘Π½Π°ΡΠ°Π»Π° ΠΊΠΎΠ½Π½Π΅ΠΊΡ, ΠΏΠΎΡΠΎΠΌ Π΄ΠΈΡΠΊΠΎΠ½Π΅ΠΊΡ");
}
}
When disconnecting, we check if the user is connected and then disconnect. It remains to send a transaction, sending occurs using sendTrx()
. In this example, immediately after the hardcoded all the data, the final code:
/// 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');
}
}
}
}
Final code
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,
)
],
),
),
));
}
}
Conclusion
Thank you for your attention, link to the example from the article, link to the library. I write similar technical articles at https://t.me/ton_learn . I will be glad to your subscription.
Top comments (0)