How to build a mobile wallet for Starknet

In this tutorial we will build a mobile wallet app.

Pre-requisites

  1. Create a new flutter project
flutter create wallet_app

Run it locally with flutter run to make sure it is properly configured.

  1. Install and run starknet-devnet

  2. Add necessary dependencies

flutter pub add wallet_kit hive_flutter hooks_riverpod flutter_dotenv
  1. Create a .env file in the root of your wallet_app project
ACCOUNT_CLASS_HASH="0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f"
RPC="http://127.0.0.1:5050/rpc"

Please note that ACCOUNT_CLASS_HASH must match the one used by your version of starknet-devnet, it's displayed at startup. Here is the value for starknet-devnet 0.2.0

Predeployed accounts using class with hash: 0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f

If you are running on another device than the host running starknet-devnet, you should use the external IP of your host running and start starknet-devnet with --host 0.0.0.0 argument

  1. Add .env file in your pubspec.yaml
  assets:
    - .env
  1. Update Android minimun SDK version

secure_store package used by wallet_kit require Android minimum SDK version set to at least 23, you need to modify android/app/build.gradle:

    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId = "com.example.wallet_app"
        // You can update the following values to match your application needs.
        // For more information, see: https://flutter.dev/to/review-gradle-config.
        minSdk = 23
        targetSdk = flutter.targetSdkVersion
        versionCode = flutter.versionCode
        versionName = flutter.versionName
    }
  1. Biometric support (optional)

In order to use Biometric on Android, your MainActivity must inherit from FlutterFragmentActivity instead of FlutterActity. You need to modify your MainActivity.kt with:

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity()

Let's write some code

Let's start with a simple main function in your 'main.dart' file.

import 'package:flutter/material.dart';

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

We will need to load our environment variables using flutter_dotenv package

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';

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

  await dotenv.load();
}

Now let's intialize wallet_kit and hive in our main function

import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:wallet_kit/wallet_kit.dart';

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

  await dotenv.load();

  await WalletKit().init(
    accountClassHash: dotenv.env['ACCOUNT_CLASS_HASH'] as String,
    rpc: dotenv.env['RPC'] as String,
  );

  await Hive.initFlutter();
}

Let's also setup device orientation and system ui overlay

SystemChrome.setEnabledSystemUIMode(
    SystemUiMode.edgeToEdge,
  );

  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);

  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.dark,
    systemNavigationBarColor: Colors.transparent,
    systemNavigationBarDividerColor: Colors.transparent,
    systemNavigationBarIconBrightness: Brightness.dark,
  ));

Finally we can create an App widget and run our app:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:wallet_kit/wallet_kit.dart';

import './screens/home_screen.dart';

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

  await dotenv.load();

  WalletKit().init(
    accountClassHash: dotenv.env['ACCOUNT_CLASS_HASH'] as String,
    rpc: dotenv.env['RPC'] as String,
  );

  await Hive.initFlutter();

  SystemChrome.setEnabledSystemUIMode(
    SystemUiMode.edgeToEdge,
  );

  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
    DeviceOrientation.portraitDown,
  ]);

  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
    statusBarColor: Colors.transparent,
    statusBarIconBrightness: Brightness.dark,
    systemNavigationBarColor: Colors.transparent,
    systemNavigationBarDividerColor: Colors.transparent,
    systemNavigationBarIconBrightness: Brightness.dark,
  ));

  runApp(const ProviderScope(child: WalletApp()));
}

class WalletApp extends HookConsumerWidget {
  const WalletApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      title: 'Starknet Wallet',
      home: const Placeholder(),
      theme: walletThemeData,
      debugShowCheckedModeBanner: false,
    );
  }
}

Create a screens/ folder and add home_screen.dart file with a pre-built layout from wallet_kit, as well as WalletSelector, AccountAddress, WalletBody and SendEthButton:

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:wallet_kit/wallet_kit.dart';

class HomeScreen extends HookConsumerWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return const Scaffold(
      body: Layout2(
        children: [
          SizedBox(height: 32),
          Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: [
              WalletSelector(),
              AccountAddress(),
              DeployAccountButton(),
            ],
          ),
          SizedBox(height: 32),
          WalletBody(),
          SendEthButton(),
        ],
      ),
    );
  }
}

Now replace home: const Placeholder() with home: const HomeScreen() in main.dart. Your WalletApp should now look like this:

import './screens/home_screen.dart';

class WalletApp extends HookConsumerWidget {
  const WalletApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
      title: 'Starknet Wallet',
      home: const HomeScreen(),
      theme: walletThemeData,
      debugShowCheckedModeBanner: false,
    );
  }
}

Now you can run your app with flutter run and see your wallet in action! 💸

Deploying an account requires some ETH to pay transaction fees. With starknet-devnet, you can mint some ETH to your account address with the following command:

curl --silent -H 'Content-type: application/json' \
  -X POST http://localhost:5050/mint \
  -d '{"address": ""<YOUR_ACCOUNT_ADDRESS>"", "amount": 20000000000000000000, "unit": "WEI"}'
{"new_balance":"20000000000000000000","unit":"WEI","tx_hash":"0x9d2d26cef777c50b64475592e0df6e6c6012014e660f97bb37aaf5138aff54"}