Skip to main content

Integrate Web3Auth with the Solana Blockchain in Flutter

While using the Web3Auth Flutter SDK, you can retrive the Ed25519 private key upon successful authentication. This private key can be used to derive user's public address and interact with Solana chain. We have highlighted a few methods here for getting you started quickly on that.

Installation

To interact with the Solana blockchain in Flutter, you can use any Solana compatible package. Here, we're using solana to demonstrate how to interact with Solana chain using Web3Auth.

flutter pub add solana get_it hex

Note: We will also be using get_it package for dependency injection, and hex package to perform Hex encoding and decoding.

Initialize

To Initialize the SolanaClient we require rpcUrl and websocketUrl. The rpcUrl will provide a gateway & protocol to interact with Solana cluster while sending requests and receving response. For this example, we are using rpcUrl & websocketUrl for Devnet-beta. To interact with Testnet or Mainnet, you can simply change the rpcUrl and websocketUrl.

Initializing Solana SDK

In the below code block, we'll initialize the SolanaClient using the Devnet-beta rpc and websocket urls. We'll also register the instance in GetIt for it to be accessed anywhere using service locator.

import 'package:flutter_solana_example/core/solana/solana_provider.dart';
import 'package:get_it/get_it.dart';
import 'package:solana/solana.dart';

class ServiceLocator {
ServiceLocator._();

static GetIt get getIt => GetIt.instance;

static Future<void> init() async {
final solanaClient = SolanaClient(
rpcUrl: Uri.parse('https://api.devnet.solana.com'),
websocketUrl: Uri.parse('ws://api.devnet.solana.com'),
);

// Register SolanaClient to be accessed using service locator
getIt.registerLazySingleton<SolanaClient>(() => solanaClient);
}
}

Initializing Web3Auth

In the below code block, we'll initialize the Web3Auth SDK and check whether the user has any Web3Auth session persisted or not. If the user is already authenticated, we can route them directly to HomeScreen, otherwise we can route them to LoginScreen.

By default, the session is persisted for 1 day. You can modify it using sessionTime parameter during initialization.

Note: The session can be persisted only for 7 days max.

// Additional imports
// ...
import 'package:web3auth_flutter/web3auth_flutter.dart';

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();

// Initialize ServiceLocator
ServiceLocator.init();

final Uri redirectUrl;
if (Platform.isAndroid) {
redirectUrl = Uri.parse('w3aexample://com.example.flutter_solana_example/auth');
} else {
redirectUrl = Uri.parse('com.web3auth.fluttersolanasample://auth');
}

await Web3AuthFlutter.init(
Web3AuthOptions(
clientId:
"BHgArYmWwSeq21czpcarYh0EVq2WWOzflX-NTK-tY1-1pauPzHKRRLgpABkmYiIV_og9jAvoIxQ8L3Smrwe04Lw",
network: Network.sapphire_devnet,
redirectUrl: redirectUrl,
),
);


runApp(const MainApp());
}

class MainApp extends StatefulWidget {
const MainApp({super.key});


State<MainApp> createState() => _MainAppState();
}

class _MainAppState extends State<MainApp> {
late final Future<String> privateKeyFuture;

void initState() {
super.initState();
privateKeyFuture = Web3AuthFlutter.getEd25519PrivKey();
}


Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder<String>(
future: privateKeyFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
// Check if user is already authenticated. If user is already
// authenticated the snapshot.data will be non empty string
// representing the private key used for Solana ecosystem.
if (snapshot.data!.isNotEmpty) {
return const HomeScreen();
}
}
return const LoginScreen();
}
return const Center(
child: CircularProgressIndicator.adaptive(),
);
},
),
);
}
}

Get User Info

After logging in, the Web3Auth instance will provide you with information regarding the user that is logged in. This information is obtained directly from the JWT token and is not stored by Web3Auth. Therefore, this information can only be accessed through social logins after the user has logged into your application.


// ..
// Additional code
// ..
class _HomeScreenState extends State<HomeScreen> {
late final ValueNotifier<bool> isAccountLoaded;
late final Ed25519HDKeyPair keyPair;
late final SolanaProvider provider;
late double balance;
late final dynamic web3AuthInfo;


void initState() {
super.initState();
isAccountLoaded = ValueNotifier<bool>(false);
provider = ServiceLocator.getIt<SolanaProvider>();
loadAccount(context);
}

Future<void> loadAccount(BuildContext context) async {
try {
final privateKey = await Web3AuthFlutter.getEd25519PrivKey();

// getUserInfo method can be used to fetch the user information
// such as email, name, isMFA enabled. Checkout documentation
// to know more.
web3AuthInfo = await Web3AuthFlutter.getUserInfo();

// ..
// Additional codebase
// ..
isAccountLoaded.value = true;
} catch (e, _) {
if (context.mounted) {
showInfoDialog(context, e.toString());
}
}
}

Widget get verticalGap => const SizedBox(height: 16);


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
logOut(context);
},
),
),
body: ValueListenableBuilder<bool>(
valueListenable: isAccountLoaded,
builder: (context, isLoaded, _) {
if (isLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
balance.toString(),
style: Theme.of(context).textTheme.displaySmall,
),
if (web3AuthInfo.email != null) ...[
verticalGap,
Text(
web3AuthInfo.email!,
style: Theme.of(context).textTheme.bodyLarge,
)
],

// ..
// Additional code
// ..

verticalGap,

// Logs the user information in the console
OutlinedButton(
onPressed: () async {
showInfoDialog(
context,
jsonEncode(web3AuthInfo.toJson()),
true,
);
},
child: const Text("Get user info"),
),
],
),
);
}
return const Center(child: CircularProgressIndicator.adaptive());
},
),
);
}

Get Account and Balance

We can use getEd25519PrivKey method in Web3Auth to retrive the priavte key for the Solana ecosystem. In the below code block, we'll use the Ed25519 private key to retive user's public address, and solana balance. We'll use SolanaProvider class to interact with Solana cluster and fetch user balance.

Setup Solana Provider

In the below code block, we'll create Solana provider to interact and perform solana operations.

import 'dart:math';

import 'package:solana/dto.dart';
import 'package:solana/solana.dart';

const int tokenDecimals = 9;

class SolanaProvider {
final SolanaClient solanaClient;

SolanaProvider(this.solanaClient);

Future<double> getBalance(String address) async {
final balanceResponse = await solanaClient.rpcClient.getBalance(
address,
);

/// We are dividing the balance by 10^9, because Solana's
/// token decimals is set to be 9;
return balanceResponse.value / pow(10, tokenDecimals);
}

// ..
// Additional methods
// ..
}

Once we have setup the SolanaProvider we'll use it to fetch user balance in the HomeScreen.

// ..
// Additional code
// ..
class _HomeScreenState extends State<HomeScreen> {
late final ValueNotifier<bool> isAccountLoaded;
late final Ed25519HDKeyPair keyPair;
late final SolanaProvider provider;
late double balance;
late final dynamic web3AuthInfo;


void initState() {
super.initState();
isAccountLoaded = ValueNotifier<bool>(false);
provider = ServiceLocator.getIt<SolanaProvider>();
loadAccount(context);
}

Future<void> loadAccount(BuildContext context) async {
try {
final privateKey = await Web3AuthFlutter.getEd25519PrivKey();

// ..
// Additional code
// ..

/// The ED25519 PrivateKey returns a key pair from
/// which we only require first 32 byte.
keyPair = await Ed25519HDKeyPair.fromPrivateKeyBytes(
privateKey: privateKey.hexToBytes.take(32).toList(),
);
balance = await provider.getBalance(keyPair.address);
isAccountLoaded.value = true;
} catch (e, _) {
if (context.mounted) {
showInfoDialog(context, e.toString());
}
}
}


Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder<bool>(
valueListenable: isAccountLoaded,
builder: (context, isLoaded, _) {
if (isLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
balance.toString(),
style: Theme.of(context).textTheme.displaySmall,
),
// ..
// Additional code
// ..
Row(
children: [
const Spacer(),
Text(
keyPair.address.addressAbbreviation,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(
width: 4,
),
IconButton(
onPressed: () {
copyContentToClipboard(context, keyPair.address);
},
icon: const Icon(Icons.copy),
),
const Spacer(),
],
),
// ..
// Additional code
// ..
],
),
);
}
return const Center(child: CircularProgressIndicator.adaptive());
},
),
);
}
}

Sign Transaction

Let's now go through how can we sign the transaction. In the below code block, we'll add a new method inside SolanaProvider we setup earlier to help us sign a transfer transaction. After successful implementation, we can use the method in HomeScreen.

class SolanaProvider {
final SolanaClient solanaClient;

SolanaProvider(this.solanaClient);

Future<String> signSendTransaction({
required Ed25519HDKeyPair keyPair,
required String destination,
required double value,
}) async {
/// Converting user input to the lamports, which are smallest value
/// in Solana.
final num lamports = value * pow(10, tokenDecimals);

final message = Message(instructions: [
SystemInstruction.transfer(
fundingAccount: keyPair.publicKey,
recipientAccount: Ed25519HDPublicKey.fromBase58(destination),
lamports: lamports.toInt(),
),
]);

final recentBlockHash = await getRecentBlockhash();

final signedTx = await signTransaction(recentBlockHash, message, [keyPair]);
return signedTx.signatures.first.toBase58();
}

Future<RecentBlockhash> getRecentBlockhash() async {
return await solanaClient.rpcClient
.getRecentBlockhash(commitment: Commitment.finalized)
.value;
}
}

// HomeScreen code

// ..
// Additional code
// ..
class _HomeScreenState extends State<HomeScreen> {

late final SolanaProvider provider;


void initState() {
super.initState();
isAccountLoaded = ValueNotifier<bool>(false);
provider = ServiceLocator.getIt<SolanaProvider>();
loadAccount(context);
}

// ..
// Additional code
// ..


Widget build(BuildContext context) {
return Scaffold(
// ..
body: ValueListenableBuilder<bool>(
valueListenable: isAccountLoaded,
builder: (context, isLoaded, _) {
if (isLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
OutlinedButton(
onPressed: () {
signSelfTransfer(context);
},
child: const Text(
"Sign Self transfer 0.0001 Sol",
),
),
],
),
);
}
return const Center(child: CircularProgressIndicator.adaptive());
},
),
);
}

Future<void> signSelfTransfer(BuildContext context) async {
showLoader(context);
try {
final signedMessage = await provider.signSendTransaction(
keyPair: keyPair,
destination: keyPair.address,
value: 0.0001,
);
if (context.mounted) {
removeDialog(context);
showInfoDialog(context, "Signed message\n$signedMessage");
}
} catch (e, _) {
if (context.mounted) {
removeDialog(context);
showInfoDialog(context, e.toString());
}
}
}
}

Send Transaction

For the send transaction, we'll create a new method sendSol in the SolanaProvider.

class SolanaProvider {
final SolanaClient solanaClient;

SolanaProvider(this.solanaClient);

Future<String> sendSol({
required Ed25519HDKeyPair keyPair,
required String destination,
required double value,
}) async {
/// Converting user input to the lamports, which are smallest value
/// in Solana.
final num lamports = value * pow(10, tokenDecimals);
final transactionHash = await solanaClient.transferLamports(
source: keyPair,
destination: Ed25519HDPublicKey.fromBase58(destination),
lamports: lamports.toInt(),
);

return transactionHash;
}
}

Once created, we can use the method in HomeScreen to transfer SOL. Upon successful transfer, we'll also refresh the balance of the user.

// ..

class _HomeScreenState extends State<HomeScreen> {
late final ValueNotifier<bool> isAccountLoaded;
late final Ed25519HDKeyPair keyPair;
late final SolanaProvider provider;
late double balance;
late final dynamic web3AuthInfo;


void initState() {
super.initState();
isAccountLoaded = ValueNotifier<bool>(false);
provider = ServiceLocator.getIt<SolanaProvider>();
loadAccount(context);
}

Future<void> refreshBalance(BuildContext context) async {
try {
isAccountLoaded.value = false;
balance = await provider.getBalance(keyPair.address);
isAccountLoaded.value = true;
} catch (e, _) {
if (context.mounted) {
showInfoDialog(context, e.toString());
}
}
}

Widget get verticalGap => const SizedBox(height: 16);


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
logOut(context);
},
),
),
body: ValueListenableBuilder<bool>(
valueListenable: isAccountLoaded,
builder: (context, isLoaded, _) {
if (isLoaded) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// ..
verticalGap,
OutlinedButton(
onPressed: () {
selfTransfer(context);
},
child: const Text(
"Self transfer 0.0001 Sol",
),
),
// ..
],
),
);
}
return const Center(child: CircularProgressIndicator.adaptive());
},
),
);
}

Future<void> selfTransfer(BuildContext context) async {
showLoader(context);
try {
final hash = await provider.sendSol(
destination: keyPair.address,
keyPair: keyPair,
value: 0.0001,
);
if (context.mounted) {
removeDialog(context);
showInfoDialog(context, "Success: $hash");
refreshBalance(context);
}
} catch (e, _) {
if (context.mounted) {
removeDialog(context);
showInfoDialog(context, e.toString());
}
}
}
}