Skip to content

Commit 8a728b2

Browse files
committed
scaffold initial architecture for bdk-dart flutter reference app
1 parent 6dfdddb commit 8a728b2

27 files changed

+1032
-167
lines changed

bdk_demo/README.md

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,82 @@
1-
# bdk_demo
1+
# BDK-Dart Wallet (Flutter)
22

3-
A new Flutter project.
3+
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.
4+
5+
The demo app is built with the following goals in mind:
6+
1. Be a reference application for the `bdk_dart` API on Flutter (iOS & Android).
7+
2. Showcase the core features of the bitcoindevkit library: wallet creation, recovery, Esplora/Electrum sync, send, receive, and transaction history.
8+
3. Demonstrate a clean, testable Flutter architecture using Riverpod and GoRouter.
9+
10+
## Features
11+
12+
| Feature | Status |
13+
|---|---|
14+
| Create wallet (P2WPKH / P2TR) | - |
15+
| Recover wallet (phrase / descriptor) | - |
16+
| Multi-wallet support | - |
17+
| Esplora sync (Regtest) | - |
18+
| Electrum sync (Testnet / Signet) | - |
19+
| Wallet balance (BTC / sats toggle) | - |
20+
| Receive (address generation + QR) | - |
21+
| Send (single recipient + fee rate) | - |
22+
| Transaction history | - |
23+
| Transaction detail | - |
24+
| Recovery data viewer | - |
25+
| Theme toggle (light / dark) | - |
26+
| In-app log viewer | - |
27+
28+
## Architecture
29+
30+
Clean Architecture + Riverpod:
31+
32+
```
33+
lib/
34+
├── app/ # App shell (MaterialApp, bootstrap)
35+
├── core/ # Theme, router, constants, logging, utils
36+
├── models/ # WalletRecord, TxDetails, CurrencyUnit
37+
├── services/ # WalletService, BlockchainService, StorageService
38+
├── providers/ # Riverpod providers (wallet, blockchain, settings)
39+
└── features/ # Feature pages and widgets
40+
```
41+
42+
**Note:**
43+
- **State management:** Riverpod
44+
- **Navigation:** GoRouter
45+
- **Domain objects:** Uses `bdk_dart` types directly
46+
- **Secure storage:** `flutter_secure_storage` for mnemonics and descriptors
47+
- **BDK threading:** `Isolate.run()` for heavy sync operations
448

549
## Getting Started
650

7-
This project is a starting point for a Flutter application.
51+
```bash
52+
# Clone and navigate to the demo app
53+
cd bdk_demo
54+
55+
# Install dependencies
56+
flutter pub get
57+
58+
# Run on a connected device or emulator
59+
flutter run
60+
```
61+
62+
> **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.
63+
64+
## Supported Networks
65+
66+
| Network | Blockchain Client | Default Endpoint |
67+
|---|---|---|
68+
| Signet | Electrum | `ssl://mempool.space:60602` |
69+
| Testnet 3 | Electrum | `ssl://electrum.blockstream.info:60002` |
70+
| Regtest | Esplora | `http://localhost:3002` |
71+
72+
73+
## Address Types
874

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

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

14-
For help getting started with Flutter development, view the
15-
[online documentation](https://docs.flutter.dev/), which offers tutorials,
16-
samples, guidance on mobile development, and a full API reference.
82+
See [LICENSE](../LICENSE).

bdk_demo/lib/app/app.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
4+
import '../core/router/app_router.dart';
5+
import '../core/theme/app_theme.dart';
6+
import '../providers/settings_providers.dart';
7+
8+
class App extends ConsumerWidget {
9+
const App({super.key});
10+
11+
@override
12+
Widget build(BuildContext context, WidgetRef ref) {
13+
final themeMode = ref.watch(themeModeProvider);
14+
final router = createRouter();
15+
16+
return MaterialApp.router(
17+
title: 'BDK-Dart Wallet',
18+
debugShowCheckedModeBanner: false,
19+
theme: AppTheme.light,
20+
darkTheme: AppTheme.dark,
21+
themeMode: themeMode,
22+
routerConfig: router,
23+
);
24+
}
25+
}

bdk_demo/lib/app/bootstrap.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
4+
5+
import '../core/logging/app_logger.dart';
6+
import '../providers/settings_providers.dart';
7+
import '../services/storage_service.dart';
8+
import 'app.dart';
9+
10+
Future<void> bootstrap() async {
11+
WidgetsFlutterBinding.ensureInitialized();
12+
13+
final prefs = await SharedPreferences.getInstance();
14+
final storageService = StorageService(prefs: prefs);
15+
16+
AppLogger.instance.info('BDK-Dart Wallet app started');
17+
18+
runApp(
19+
ProviderScope(
20+
overrides: [
21+
storageServiceProvider.overrideWithValue(storageService),
22+
],
23+
child: const App(),
24+
),
25+
);
26+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:bdk_demo/models/wallet_record.dart';
2+
3+
abstract final class AppConstants {
4+
static const appVersion = '0.1.0';
5+
6+
static const walletLookahead = 25;
7+
8+
static const fullScanStopGap = 25;
9+
10+
static const syncParallelRequests = 4;
11+
12+
static const maxRecipients = 4;
13+
14+
static const maxLogEntries = 5000;
15+
}
16+
17+
enum ClientType { esplora, electrum }
18+
19+
class EndpointConfig {
20+
final ClientType clientType;
21+
final String url;
22+
23+
const EndpointConfig({required this.clientType, required this.url});
24+
}
25+
26+
const Map<WalletNetwork, EndpointConfig> defaultEndpoints = {
27+
WalletNetwork.signet: EndpointConfig(
28+
clientType: ClientType.electrum,
29+
url: 'ssl://mempool.space:60602',
30+
),
31+
WalletNetwork.testnet: EndpointConfig(
32+
clientType: ClientType.electrum,
33+
url: 'ssl://electrum.blockstream.info:60002',
34+
),
35+
WalletNetwork.regtest: EndpointConfig(
36+
clientType: ClientType.esplora,
37+
url: 'http://localhost:3002',
38+
),
39+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'dart:collection';
2+
3+
import 'package:bdk_demo/core/constants/app_constants.dart';
4+
5+
enum LogLevel { info, warn, error }
6+
7+
class AppLogger {
8+
AppLogger._();
9+
10+
static final AppLogger instance = AppLogger._();
11+
12+
final _entries = Queue<String>();
13+
14+
int get maxEntries => AppConstants.maxLogEntries;
15+
16+
void log(LogLevel level, String message) {
17+
final timestamp = DateTime.now().toIso8601String();
18+
final label = switch (level) {
19+
LogLevel.info => 'INFO',
20+
LogLevel.warn => 'WARN',
21+
LogLevel.error => 'ERROR',
22+
};
23+
_entries.addFirst('$timestamp [$label] $message');
24+
while (_entries.length > maxEntries) {
25+
_entries.removeLast();
26+
}
27+
}
28+
29+
void info(String message) => log(LogLevel.info, message);
30+
void warn(String message) => log(LogLevel.warn, message);
31+
void error(String message) => log(LogLevel.error, message);
32+
33+
List<String> getLogs() => _entries.toList();
34+
35+
void clear() => _entries.clear();
36+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import 'package:go_router/go_router.dart';
2+
import 'package:bdk_demo/features/shared/widgets/placeholder_page.dart';
3+
import 'package:bdk_demo/features/wallet_setup/wallet_choice_page.dart';
4+
5+
abstract final class AppRoutes {
6+
static const walletChoice = '/';
7+
static const activeWallets = '/active-wallets';
8+
static const createWallet = '/create-wallet';
9+
static const recoverWallet = '/recover-wallet';
10+
static const home = '/home';
11+
static const receive = '/receive';
12+
static const send = '/send';
13+
static const transactionHistory = '/transactions';
14+
static const transactionDetail = '/transactions/:txid';
15+
static const settings = '/settings';
16+
static const about = '/about';
17+
static const theme = '/theme';
18+
static const logs = '/logs';
19+
static const recoveryData = '/recovery-data';
20+
}
21+
22+
GoRouter createRouter() => GoRouter(
23+
initialLocation: AppRoutes.walletChoice,
24+
routes: [
25+
GoRoute(
26+
path: AppRoutes.walletChoice,
27+
name: 'walletChoice',
28+
builder: (context, state) => const WalletChoicePage(),
29+
),
30+
GoRoute(
31+
path: AppRoutes.activeWallets,
32+
name: 'activeWallets',
33+
builder: (context, state) =>
34+
const PlaceholderPage(title: 'Active Wallets'),
35+
),
36+
GoRoute(
37+
path: AppRoutes.createWallet,
38+
name: 'createWallet',
39+
builder: (context, state) =>
40+
const PlaceholderPage(title: 'Create Wallet'),
41+
),
42+
GoRoute(
43+
path: AppRoutes.recoverWallet,
44+
name: 'recoverWallet',
45+
builder: (context, state) =>
46+
const PlaceholderPage(title: 'Recover Wallet'),
47+
),
48+
49+
GoRoute(
50+
path: AppRoutes.home,
51+
name: 'home',
52+
builder: (context, state) => const PlaceholderPage(title: 'Home'),
53+
),
54+
GoRoute(
55+
path: AppRoutes.receive,
56+
name: 'receive',
57+
builder: (context, state) => const PlaceholderPage(title: 'Receive'),
58+
),
59+
GoRoute(
60+
path: AppRoutes.send,
61+
name: 'send',
62+
builder: (context, state) => const PlaceholderPage(title: 'Send'),
63+
),
64+
GoRoute(
65+
path: AppRoutes.transactionHistory,
66+
name: 'transactionHistory',
67+
builder: (context, state) =>
68+
const PlaceholderPage(title: 'Transaction History'),
69+
),
70+
GoRoute(
71+
path: AppRoutes.transactionDetail,
72+
name: 'transactionDetail',
73+
builder: (context, state) {
74+
final txid = state.pathParameters['txid'] ?? '';
75+
return PlaceholderPage(title: 'Transaction $txid');
76+
},
77+
),
78+
79+
GoRoute(
80+
path: AppRoutes.settings,
81+
name: 'settings',
82+
builder: (context, state) => const PlaceholderPage(title: 'Settings'),
83+
),
84+
GoRoute(
85+
path: AppRoutes.about,
86+
name: 'about',
87+
builder: (context, state) => const PlaceholderPage(title: 'About'),
88+
),
89+
GoRoute(
90+
path: AppRoutes.theme,
91+
name: 'theme',
92+
builder: (context, state) => const PlaceholderPage(title: 'Theme'),
93+
),
94+
GoRoute(
95+
path: AppRoutes.logs,
96+
name: 'logs',
97+
builder: (context, state) => const PlaceholderPage(title: 'Logs'),
98+
),
99+
GoRoute(
100+
path: AppRoutes.recoveryData,
101+
name: 'recoveryData',
102+
builder: (context, state) =>
103+
const PlaceholderPage(title: 'Recovery Data'),
104+
),
105+
],
106+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'package:flutter/material.dart';
2+
3+
abstract final class AppColors {
4+
static const dayGlowPrimary = Color(0xFFF7931A);
5+
static const dayGlowOnPrimary = Colors.white;
6+
static const dayGlowBackground = Color(0xFFFAFAFA);
7+
static const dayGlowSurface = Colors.white;
8+
static const dayGlowHistoryAccent = Color(0xFFF5A623);
9+
10+
static const nightGlowPrimary = Color(0xFFF7931A);
11+
static const nightGlowOnPrimary = Colors.white;
12+
static const nightGlowBackground = Color(0xFF121212);
13+
static const nightGlowSurface = Color(0xFF1E1E1E);
14+
static const nightGlowSubtle = Color(0xFF2A2A2A);
15+
16+
static const pendingOrange = Color(0xFFF5A623);
17+
static const confirmedGreen = Color(0xFF8FD998);
18+
static const offlineRed = Color(0xFFE76F51);
19+
static const errorRed = Color(0xFFCF6679);
20+
}

0 commit comments

Comments
 (0)