Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 75 additions & 9 deletions bdk_demo/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,82 @@
# bdk_demo
# BDK-Dart Wallet (Flutter)

A new Flutter project.
The _BDK-Dart Wallet_ is a wallet built as a reference app for the [bitcoindevkit](https://github.com/bitcoindevkit) on Flutter using [bdk-dart](https://github.com/bitcoindevkit/bdk-dart). This repository is not intended to produce a production-ready wallet, the app only works on Signet, Testnet 3, and Regtest.

The demo app is built with the following goals in mind:
1. Be a reference application for the `bdk_dart` API on Flutter (iOS & Android).
2. Showcase the core features of the bitcoindevkit library: wallet creation, recovery, Esplora/Electrum sync, send, receive, and transaction history.
3. Demonstrate a clean, testable Flutter architecture using Riverpod and GoRouter.

## Features

| Feature | Status |
|---|---|
| Create wallet (P2WPKH / P2TR) | - |
| Recover wallet (phrase / descriptor) | - |
| Multi-wallet support | - |
| Esplora sync (Regtest) | - |
| Electrum sync (Testnet / Signet) | - |
| Wallet balance (BTC / sats toggle) | - |
| Receive (address generation + QR) | - |
| Send (single recipient + fee rate) | - |
| Transaction history | - |
| Transaction detail | - |
| Recovery data viewer | - |
| Theme toggle (light / dark) | - |
| In-app log viewer | - |

## Architecture

Clean Architecture + Riverpod:

```
lib/
├── app/ # App shell (MaterialApp, bootstrap)
├── core/ # Theme, router, constants, logging, utils
├── models/ # WalletRecord, TxDetails, CurrencyUnit
├── services/ # WalletService, BlockchainService, StorageService
├── providers/ # Riverpod providers (wallet, blockchain, settings)
└── features/ # Feature pages and widgets
```

**Note:**
- **State management:** Riverpod
- **Navigation:** GoRouter
- **Domain objects:** Uses `bdk_dart` types directly
- **Secure storage:** `flutter_secure_storage` for mnemonics and descriptors
- **BDK threading:** `Isolate.run()` for heavy sync operations

## Getting Started

This project is a starting point for a Flutter application.
```bash
# Clone and navigate to the demo app
cd bdk_demo

# Install dependencies
flutter pub get

# Run on a connected device or emulator
flutter run
```

> **Note:** This app depends on `bdk_dart` via a local path (`../`). Make sure the parent `bdk-dart` repository is set up and the native Rust build toolchain is available. See the [bdk-dart README](../README.md) for build prerequisites.

## Supported Networks

| Network | Blockchain Client | Default Endpoint |
|---|---|---|
| Signet | Electrum | `ssl://mempool.space:60602` |
| Testnet 3 | Electrum | `ssl://electrum.blockstream.info:60002` |
| Regtest | Esplora | `http://localhost:3002` |


## Address Types

A few resources to get you started if this is your first Flutter project:
| Type | Standard | Default |
|---|---|---|
| P2TR (Taproot) | BIP-86 | - |
| P2WPKH (Native SegWit) | BIP-84 | - |

- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
## License

For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
See [LICENSE](../LICENSE).
25 changes: 25 additions & 0 deletions bdk_demo/lib/app/app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers/router_provider.dart';
import '../core/theme/app_theme.dart';
import '../providers/settings_providers.dart';

class App extends ConsumerWidget {
const App({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final router = ref.watch(appRouterProvider);

return MaterialApp.router(
title: 'BDK-Dart Wallet',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: themeMode,
routerConfig: router,
);
}
}
24 changes: 24 additions & 0 deletions bdk_demo/lib/app/bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../core/logging/app_logger.dart';
import '../providers/settings_providers.dart';
import '../services/storage_service.dart';
import 'app.dart';

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

final prefs = await SharedPreferences.getInstance();
final storageService = StorageService(prefs: prefs);

AppLogger.instance.info('BDK-Dart Wallet app started');

runApp(
ProviderScope(
overrides: [storageServiceProvider.overrideWithValue(storageService)],
child: const App(),
),
);
}
39 changes: 39 additions & 0 deletions bdk_demo/lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:bdk_demo/models/wallet_record.dart';

abstract final class AppConstants {
static const appVersion = '0.1.0';

static const walletLookahead = 25;

static const fullScanStopGap = 25;

static const syncParallelRequests = 4;

static const maxRecipients = 4;

static const maxLogEntries = 5000;
}

enum ClientType { esplora, electrum }

class EndpointConfig {
final ClientType clientType;
final String url;

const EndpointConfig({required this.clientType, required this.url});
}

const Map<WalletNetwork, EndpointConfig> defaultEndpoints = {
WalletNetwork.signet: EndpointConfig(
clientType: ClientType.electrum,
url: 'ssl://mempool.space:60602',
),
WalletNetwork.testnet: EndpointConfig(
clientType: ClientType.electrum,
url: 'ssl://electrum.blockstream.info:60002',
),
WalletNetwork.regtest: EndpointConfig(
clientType: ClientType.esplora,
url: 'http://localhost:3002',
),
};
36 changes: 36 additions & 0 deletions bdk_demo/lib/core/logging/app_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'dart:collection';

import 'package:bdk_demo/core/constants/app_constants.dart';

enum LogLevel { info, warn, error }

class AppLogger {
AppLogger._();

static final AppLogger instance = AppLogger._();

final _entries = Queue<String>();

int get maxEntries => AppConstants.maxLogEntries;

void log(LogLevel level, String message) {
final timestamp = DateTime.now().toIso8601String();
final label = switch (level) {
LogLevel.info => 'INFO',
LogLevel.warn => 'WARN',
LogLevel.error => 'ERROR',
};
_entries.addFirst('$timestamp [$label] $message');
while (_entries.length > maxEntries) {
_entries.removeLast();
}
}

void info(String message) => log(LogLevel.info, message);
void warn(String message) => log(LogLevel.warn, message);
void error(String message) => log(LogLevel.error, message);

List<String> getLogs() => _entries.toList();

void clear() => _entries.clear();
}
106 changes: 106 additions & 0 deletions bdk_demo/lib/core/router/app_router.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'package:go_router/go_router.dart';
import 'package:bdk_demo/features/shared/widgets/placeholder_page.dart';
import 'package:bdk_demo/features/wallet_setup/wallet_choice_page.dart';

abstract final class AppRoutes {
static const walletChoice = '/';
static const activeWallets = '/active-wallets';
static const createWallet = '/create-wallet';
static const recoverWallet = '/recover-wallet';
static const home = '/home';
static const receive = '/receive';
static const send = '/send';
static const transactionHistory = '/transactions';
static const transactionDetail = '/transactions/:txid';
static const settings = '/settings';
static const about = '/about';
static const theme = '/theme';
static const logs = '/logs';
static const recoveryData = '/recovery-data';
}

GoRouter createRouter() => GoRouter(
initialLocation: AppRoutes.walletChoice,
routes: [
GoRoute(
path: AppRoutes.walletChoice,
name: 'walletChoice',
builder: (context, state) => const WalletChoicePage(),
),
GoRoute(
path: AppRoutes.activeWallets,
name: 'activeWallets',
builder: (context, state) =>
const PlaceholderPage(title: 'Active Wallets'),
),
GoRoute(
path: AppRoutes.createWallet,
name: 'createWallet',
builder: (context, state) =>
const PlaceholderPage(title: 'Create Wallet'),
),
GoRoute(
path: AppRoutes.recoverWallet,
name: 'recoverWallet',
builder: (context, state) =>
const PlaceholderPage(title: 'Recover Wallet'),
),

GoRoute(
path: AppRoutes.home,
name: 'home',
builder: (context, state) => const PlaceholderPage(title: 'Home'),
),
GoRoute(
path: AppRoutes.receive,
name: 'receive',
builder: (context, state) => const PlaceholderPage(title: 'Receive'),
),
GoRoute(
path: AppRoutes.send,
name: 'send',
builder: (context, state) => const PlaceholderPage(title: 'Send'),
),
GoRoute(
path: AppRoutes.transactionHistory,
name: 'transactionHistory',
builder: (context, state) =>
const PlaceholderPage(title: 'Transaction History'),
),
GoRoute(
path: AppRoutes.transactionDetail,
name: 'transactionDetail',
builder: (context, state) {
final txid = state.pathParameters['txid'] ?? '';
return PlaceholderPage(title: 'Transaction $txid');
},
),

GoRoute(
path: AppRoutes.settings,
name: 'settings',
builder: (context, state) => const PlaceholderPage(title: 'Settings'),
),
GoRoute(
path: AppRoutes.about,
name: 'about',
builder: (context, state) => const PlaceholderPage(title: 'About'),
),
GoRoute(
path: AppRoutes.theme,
name: 'theme',
builder: (context, state) => const PlaceholderPage(title: 'Theme'),
),
GoRoute(
path: AppRoutes.logs,
name: 'logs',
builder: (context, state) => const PlaceholderPage(title: 'Logs'),
),
GoRoute(
path: AppRoutes.recoveryData,
name: 'recoveryData',
builder: (context, state) =>
const PlaceholderPage(title: 'Recovery Data'),
),
],
);
20 changes: 20 additions & 0 deletions bdk_demo/lib/core/theme/app_colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';

abstract final class AppColors {
static const dayGlowPrimary = Color(0xFFF7931A);
static const dayGlowOnPrimary = Colors.white;
static const dayGlowBackground = Color(0xFFFAFAFA);
static const dayGlowSurface = Colors.white;
static const dayGlowHistoryAccent = Color(0xFFF5A623);

static const nightGlowPrimary = Color(0xFFF7931A);
static const nightGlowOnPrimary = Colors.white;
static const nightGlowBackground = Color(0xFF121212);
static const nightGlowSurface = Color(0xFF1E1E1E);
static const nightGlowSubtle = Color(0xFF2A2A2A);

static const pendingOrange = Color(0xFFF5A623);
static const confirmedGreen = Color(0xFF8FD998);
static const offlineRed = Color(0xFFE76F51);
static const errorRed = Color(0xFFCF6679);
}
50 changes: 50 additions & 0 deletions bdk_demo/lib/core/theme/app_theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';

import 'app_colors.dart';

abstract final class AppTheme {
static ThemeData get light => ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.dayGlowPrimary,
brightness: Brightness.light,
surface: AppColors.dayGlowSurface,
),
scaffoldBackgroundColor: AppColors.dayGlowBackground,
textTheme: GoogleFonts.interTextTheme(ThemeData.light().textTheme),
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
),
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
);

static ThemeData get dark => ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColors.nightGlowPrimary,
brightness: Brightness.dark,
surface: AppColors.nightGlowSurface,
),
scaffoldBackgroundColor: AppColors.nightGlowBackground,
textTheme: GoogleFonts.interTextTheme(ThemeData.dark().textTheme),
appBarTheme: const AppBarTheme(centerTitle: true, elevation: 0),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade800),
),
),
snackBarTheme: const SnackBarThemeData(behavior: SnackBarBehavior.floating),
);

static TextStyle get monoStyle => GoogleFonts.sourceCodePro();
}
Loading
Loading