This example demonstrates how to integrate MetaMask Embedded Wallets with Solana blockchain in a Flutter application. It showcases social authentication combined with Solana wallet operations including SOL transfers, SPL token management, and NFT interactions.
- Social Authentication: Google, Facebook, Twitter, Discord, Apple, and more
- Solana Wallet: Automatic Ed25519 Solana wallet creation
- SOL Operations: Check balance, send SOL, request airdrops (devnet/testnet)
- SPL Token Support: Token balances, transfers, and token account management
- NFT Support: View and manage Solana NFTs
- Secure Key Management: Non-custodial Ed25519 key management
- Cross-Platform: Single codebase for iOS and Android
- Devnet & Mainnet: Easily switch between Solana networks
This example uses web3auth_flutter ^8.0.0 with explicit Solana chain config in Web3AuthOptions and getEd25519PrivateKey(). See the root README.
- iOS 15, native Web3Auth from Swift Package Manager
sessionTimecomes from the dashboard- Email passwordless login uses
MFALevel.OPTIONAL(MFALevel.DEFAULTwas removed)
- Flutter: 3.0.0 or higher
- Dart: 2.18.0 or higher
- MetaMask Embedded Wallets: Dashboard account
- Solana Knowledge: Basic understanding of Solana blockchain
- iOS (for iOS development):
- iOS 15+, Xcode 15+, Swift 5.x, CocoaPods (other plugins) + Swift Package Manager (Web3Auth)
- Android (for Android development):
- API level 26+, compileSdkVersion 34, JDK 11+
-
Clone the repository:
git clone https://github.com/Web3Auth/web3auth-flutter-examples.git cd web3auth-flutter-examples/flutter-solana-example -
Install dependencies:
flutter pub get
-
iOS Setup (for iOS development):
cd ios && pod install && cd ..
-
Create a project on the Embedded Wallets Dashboard
-
Choose your network:
- Sapphire Devnet: For development/testing
- Sapphire Mainnet: For production
-
Configure platform settings:
- iOS: Allowlist
{bundleId}://auth(e.g.,com.example.solanaapp://auth) - Android: Allowlist your package name and configure deep link scheme
- iOS: Allowlist
-
Get your Client ID from the dashboard
Update the configuration in lib/main.dart:
import 'package:web3auth_flutter/web3auth_flutter.dart';
import 'dart:io';
Future<void> initWeb3Auth() async {
String redirectUrl;
if (Platform.isAndroid) {
redirectUrl = 'w3a://com.example.solanaapp/auth';
} else if (Platform.isIOS) {
redirectUrl = 'com.example.solanaapp://auth';
}
await Web3AuthFlutter.init(
Web3AuthOptions(
clientId: "YOUR_WEB3AUTH_CLIENT_ID",
web3AuthNetwork: Web3AuthNetwork.sapphire_mainnet, // or Web3AuthNetwork.sapphire_devnet
redirectUrl: redirectUrl,
)
);
await Web3AuthFlutter.initialize();
}Configure Solana network in lib/config/solana_config.dart:
class SolanaConfig {
// Solana Devnet (for testing)
static const String devnetRpcUrl = 'https://api.devnet.solana.com';
// Solana Mainnet (for production)
static const String mainnetRpcUrl = 'https://api.mainnet-beta.solana.com';
// Or use a dedicated RPC provider for better performance
// static const String mainnetRpcUrl = 'https://solana-mainnet.g.alchemy.com/v2/YOUR_API_KEY';
// Current environment
static const bool useDevnet = true; // Set to false for mainnet
static String get rpcUrl => useDevnet ? devnetRpcUrl : mainnetRpcUrl;
}# Run in debug mode
flutter run
# Build for release
flutter build ios # For iOS
flutter build apk # For Androidlib/
βββ main.dart # Entry point & initialization
βββ config/
β βββ solana_config.dart # Solana network configuration
βββ services/
β βββ web3auth_service.dart # Web3Auth operations
β βββ solana_service.dart # Solana blockchain operations
βββ screens/
βββ home.dart # Home screen with wallet info
βββ login.dart # Login screen
import 'package:web3auth_flutter/web3auth_flutter.dart';
Future<void> initWeb3Auth() async {
// ... (see Code Configuration section above)
}// Login with Google
Future<void> login() async {
try {
final Web3AuthResponse response = await Web3AuthFlutter.connectTo(
LoginParams(
authConnection: AuthConnection.google,
mfaLevel: MFALevel.NONE,
)
);
print('User logged in: ${response.userInfo?.email}');
} catch (e) {
print('Login error: $e');
}
}
// Logout
Future<void> logout() async {
await Web3AuthFlutter.logout();
}// IMPORTANT: For Solana, use getEd25519PrivateKey(), not getPrivateKey()
Future<String> getSolanaPrivateKey() async {
final privateKeyHex = await Web3AuthFlutter.getEd25519PrivateKey();
return privateKeyHex;
}import 'package:solana/solana.dart';
import 'dart:typed_data';
Future<Ed25519HDKeyPair> getSolanaKeyPair() async {
// Get Ed25519 private key from Web3Auth
final privateKeyHex = await Web3AuthFlutter.getEd25519PrivateKey();
// Convert hex to bytes
final privateKeyBytes = Uint8List.fromList(
List<int>.generate(
privateKeyHex.length ~/ 2,
(i) => int.parse(privateKeyHex.substring(i * 2, i * 2 + 2), radix: 16),
),
);
// Create KeyPair
final keyPair = await Ed25519HDKeyPair.fromPrivateKeyBytes(
privateKey: privateKeyBytes,
);
return keyPair;
}Future<String> getSolanaAddress() async {
final keyPair = await getSolanaKeyPair();
return keyPair.address;
}import 'package:solana/solana.dart';
Future<double> getBalance() async {
final keyPair = await getSolanaKeyPair();
// Create Solana client
final client = SolanaClient(
rpcUrl: Uri.parse(SolanaConfig.rpcUrl),
websocketUrl: Uri.parse(
SolanaConfig.rpcUrl.replaceFirst('https://', 'wss://').replaceFirst('http://', 'ws://'),
),
);
// Get balance
final balance = await client.rpcClient.getBalance(keyPair.address);
// Convert lamports to SOL (1 SOL = 1,000,000,000 lamports)
final solBalance = balance.value / 1e9;
return solBalance;
}Future<String> requestAirdrop() async {
final keyPair = await getSolanaKeyPair();
final client = SolanaClient(
rpcUrl: Uri.parse(SolanaConfig.devnetRpcUrl),
websocketUrl: Uri.parse('wss://api.devnet.solana.com'),
);
// Request 1 SOL airdrop
final signature = await client.rpcClient.requestAirdrop(
address: keyPair.address,
lamports: 1000000000, // 1 SOL
);
print('Airdrop signature: $signature');
return signature;
}Future<String> sendSol(String recipientAddress, double amount) async {
final keyPair = await getSolanaKeyPair();
final client = SolanaClient(
rpcUrl: Uri.parse(SolanaConfig.rpcUrl),
websocketUrl: Uri.parse(
SolanaConfig.rpcUrl.replaceFirst('https://', 'wss://').replaceFirst('http://', 'ws://'),
),
);
// Convert SOL to lamports
final lamports = (amount * 1e9).toInt();
// Create and send transaction
final signature = await client.transferLamports(
source: keyPair,
destination: Ed25519HDPublicKey.fromBase58(recipientAddress),
lamports: lamports,
);
print('Transaction signature: $signature');
return signature;
}import 'package:solana/solana.dart';
Future<double> getTokenBalance(String tokenMintAddress) async {
final keyPair = await getSolanaKeyPair();
final client = SolanaClient(
rpcUrl: Uri.parse(SolanaConfig.rpcUrl),
websocketUrl: Uri.parse(
SolanaConfig.rpcUrl.replaceFirst('https://', 'wss://').replaceFirst('http://', 'ws://'),
),
);
// Get token accounts for the wallet
final tokenAccounts = await client.rpcClient.getTokenAccountsByOwner(
keyPair.address,
TokenAccountsFilter.byMint(tokenMintAddress),
encoding: Encoding.jsonParsed,
);
if (tokenAccounts.value.isEmpty) {
return 0.0;
}
// Parse token balance
final accountInfo = tokenAccounts.value.first.account.data;
if (accountInfo is ParsedAccountData) {
final tokenAmount = accountInfo.parsed['info']['tokenAmount'];
return double.parse(tokenAmount['uiAmountString']);
}
return 0.0;
}Future<List<String>> getNFTs() async {
final keyPair = await getSolanaKeyPair();
final client = SolanaClient(
rpcUrl: Uri.parse(SolanaConfig.rpcUrl),
websocketUrl: Uri.parse(
SolanaConfig.rpcUrl.replaceFirst('https://', 'wss://').replaceFirst('http://', 'ws://'),
),
);
// Get all token accounts
final tokenAccounts = await client.rpcClient.getTokenAccountsByOwner(
keyPair.address,
TokenAccountsFilter.byProgramId(TokenProgram.programId),
encoding: Encoding.jsonParsed,
);
// Filter for NFTs (amount = 1, decimals = 0)
List<String> nftMints = [];
for (var account in tokenAccounts.value) {
if (account.account.data is ParsedAccountData) {
final data = account.account.data as ParsedAccountData;
final tokenAmount = data.parsed['info']['tokenAmount'];
if (tokenAmount['decimals'] == 0 &&
tokenAmount['amount'] == '1') {
// This is likely an NFT
nftMints.add(data.parsed['info']['mint']);
}
}
}
return nftMints;
}- Ed25519 Keys: Solana uses Ed25519 curve, not secp256k1 - use
getEd25519PrivateKey() - Non-Custodial: Private keys managed using Shamir Secret Sharing
- Network Consistency: Never change Client ID or Sapphire network in production
- Devnet vs Mainnet: Always test on devnet before deploying to mainnet
- RPC Rate Limits: Use dedicated RPC providers (Alchemy, QuickNode, Helius) for production
- Transaction Fees: Ensure wallet has sufficient SOL for transaction fees
Problem: Different wallet address than expected
Solution:
- Ensure you're using
getEd25519PrivateKey()notgetPrivateKey() - Verify you're using the same Client ID and network
Problem: Airdrop fails
Solutions:
- Airdrops only work on devnet/testnet
- Rate limits: Wait a few minutes between airdrop requests
- Use alternative devnet/testnet faucets
Problem: Transaction fails with "insufficient funds"
Solutions:
- Check SOL balance for transaction fees (~0.000005 SOL per signature)
- Request devnet airdrop or transfer SOL from another wallet
Problem: Token balance shows 0 but tokens exist
Solutions:
- Verify token mint address is correct
- Check that token account exists for the wallet
- Ensure using correct Solana network (devnet vs mainnet)
Problem: RPC errors or slow responses
Solutions:
- Use dedicated RPC provider (Alchemy, QuickNode, Helius) instead of public endpoints
- Implement retry logic with exponential backoff
- Check RPC provider status page
- MetaMask Embedded Wallets Docs
- Flutter SDK Reference
- Solana Documentation
- Solana Cookbook
- Metaplex Documentation
Need help? Reach out through:
This example is available under the MIT License. See the LICENSE file for more info.