From 5a8300c5ea44971d18b30d25de7f96968196389e Mon Sep 17 00:00:00 2001 From: StillHammer Date: Sun, 1 Feb 2026 10:12:34 +0800 Subject: [PATCH] Initial setup: Flutter blockchain explorer app - Clean architecture with feature-based organization - Riverpod state management, Dio HTTP, GoRouter navigation - Dashboard, blocks, wallet, transactions, mining screens - Dark crypto theme with JetBrains Mono font --- .gitignore | 30 +++++ README.md | 100 +++++++++++++++++ lib/core/network/api_client.dart | 105 ++++++++++++++++++ lib/core/theme/app_theme.dart | 49 ++++++++ lib/core/utils/formatters.dart | 31 ++++++ lib/features/blocks/block_detail_screen.dart | 21 ++++ lib/features/blocks/blocks_screen.dart | 19 ++++ lib/features/dashboard/dashboard_screen.dart | 19 ++++ lib/features/mining/mining_screen.dart | 19 ++++ lib/features/transactions/send_tx_screen.dart | 19 ++++ .../transactions/transactions_screen.dart | 19 ++++ lib/features/wallet/wallet_screen.dart | 19 ++++ lib/main.dart | 25 +++++ lib/routing/app_router.dart | 95 ++++++++++++++++ pubspec.yaml | 34 ++++++ 15 files changed, 604 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 lib/core/network/api_client.dart create mode 100644 lib/core/theme/app_theme.dart create mode 100644 lib/core/utils/formatters.dart create mode 100644 lib/features/blocks/block_detail_screen.dart create mode 100644 lib/features/blocks/blocks_screen.dart create mode 100644 lib/features/dashboard/dashboard_screen.dart create mode 100644 lib/features/mining/mining_screen.dart create mode 100644 lib/features/transactions/send_tx_screen.dart create mode 100644 lib/features/transactions/transactions_screen.dart create mode 100644 lib/features/wallet/wallet_screen.dart create mode 100644 lib/main.dart create mode 100644 lib/routing/app_router.dart create mode 100644 pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e5a6cde --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ +*.iml +*.ipr +*.iws +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c8d678 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# blockchain-flutter + +Flutter app for blockchain explorer, wallet management, and mining. Connects to the blockchain node via REST API. + +## Architecture + +Clean Architecture with feature-based organization: + +``` +lib/ +├── main.dart +├── core/ +│ ├── network/ +│ │ └── api_client.dart # Dio HTTP client setup +│ ├── theme/ +│ │ └── app_theme.dart # Dark theme, crypto aesthetic +│ └── utils/ +│ └── formatters.dart # Amount, hash, date formatting +├── features/ +│ ├── dashboard/ # Home: chain stats, recent blocks +│ │ ├── dashboard_screen.dart +│ │ └── dashboard_provider.dart +│ ├── blocks/ # Block explorer (list + detail) +│ │ ├── blocks_screen.dart +│ │ ├── block_detail_screen.dart +│ │ └── blocks_provider.dart +│ ├── wallet/ # Create, view balance, list wallets +│ │ ├── wallet_screen.dart +│ │ └── wallet_provider.dart +│ ├── transactions/ # Send tx, pending list +│ │ ├── transactions_screen.dart +│ │ ├── send_tx_screen.dart +│ │ └── transactions_provider.dart +│ └── mining/ # Mine button, difficulty info +│ ├── mining_screen.dart +│ └── mining_provider.dart +└── routing/ + └── app_router.dart # GoRouter navigation +``` + +## Tech Stack + +| Component | Choice | Rationale | +|-----------|--------|-----------| +| State management | Riverpod | Less boilerplate than BLoC, familiar | +| HTTP client | Dio | Interceptors, retry support | +| Navigation | GoRouter | Declarative routing | +| Models | Freezed + json_serializable | Immutable data classes | +| Secure storage | flutter_secure_storage | Wallet private keys | +| Charts | fl_chart | Chain visualization | +| Real-time | Polling (5-10s) | Simple, no WebSocket needed | + +## Features + +- **Dashboard**: chain height, difficulty, block reward, recent blocks +- **Block Explorer**: browse blocks, view transactions per block +- **Wallet**: generate keypairs, check balances, manage multiple wallets +- **Transactions**: send signed transactions, view pending pool +- **Mining**: one-tap mining, difficulty display, reward info + +## Screens + +1. **Dashboard** - overview with chain stats and recent activity +2. **Blocks** - paginated block list with expandable details +3. **Block Detail** - full block info with transaction list +4. **Wallets** - wallet list with balances, create new +5. **Send Transaction** - form: from, to, amount, sign & submit +6. **Pending Transactions** - current mempool +7. **Mining** - mine button with result display + +## Quick Start + +```bash +# Install dependencies +flutter pub get + +# Generate code (Freezed models) +dart run build_runner build --delete-conflicting-outputs + +# Run (blockchain-node must be running on localhost:3000) +flutter run + +# Run on web +flutter run -d chrome +``` + +## Configuration + +Node URL is configured in `lib/core/network/api_client.dart`. Defaults to `http://localhost:3000`. + +For Android emulator, use `http://10.0.2.2:3000` to reach host localhost. + +## Related Repos + +- [blockchain-core](../blockchain-core) - Core library + REST API node +- [blockchain-cli](../blockchain-cli) - Rust CLI tool + +## License + +MIT diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart new file mode 100644 index 0000000..57b7094 --- /dev/null +++ b/lib/core/network/api_client.dart @@ -0,0 +1,105 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +class ApiClient { + static const String _defaultBaseUrl = 'http://localhost:3000'; + // Use 10.0.2.2 for Android emulator to reach host localhost + static const String _androidEmulatorUrl = 'http://10.0.2.2:3000'; + + late final Dio dio; + + ApiClient({String? baseUrl}) { + final url = baseUrl ?? _resolveBaseUrl(); + + dio = Dio(BaseOptions( + baseUrl: url, + connectTimeout: const Duration(seconds: 5), + receiveTimeout: const Duration(seconds: 10), + headers: {'Content-Type': 'application/json'}, + )); + + dio.interceptors.add(LogInterceptor( + requestBody: true, + responseBody: true, + logPrint: (log) => debugPrint(log.toString()), + )); + } + + static String _resolveBaseUrl() { + // Android emulator uses 10.0.2.2 to reach host machine + if (defaultTargetPlatform == TargetPlatform.android) { + return _androidEmulatorUrl; + } + return _defaultBaseUrl; + } + + // Chain + Future> getChainInfo() async { + final response = await dio.get('/api/chain/info'); + return response.data; + } + + Future> validateChain() async { + final response = await dio.post('/api/chain/validate'); + return response.data; + } + + // Blocks + Future> getBlocks({int offset = 0, int limit = 10}) async { + final response = await dio.get('/api/blocks', queryParameters: { + 'offset': offset, + 'limit': limit, + }); + return response.data; + } + + Future> getBlock(String hash) async { + final response = await dio.get('/api/blocks/$hash'); + return response.data; + } + + // Wallets + Future> createWallet() async { + final response = await dio.post('/api/wallets'); + return response.data; + } + + Future> getBalance(String address) async { + final response = await dio.get('/api/wallets/$address/balance'); + return response.data; + } + + // Transactions + Future> submitTransaction(Map tx) async { + final response = await dio.post('/api/transactions', data: tx); + return response.data; + } + + Future> getPendingTransactions() async { + final response = await dio.get('/api/transactions/pending'); + return response.data; + } + + // Mining + Future> mine(String minerAddress) async { + final response = await dio.post('/api/mine', data: { + 'miner_address': minerAddress, + }); + return response.data; + } + + Future> getMiningStatus() async { + final response = await dio.get('/api/mining/status'); + return response.data; + } + + // Health + Future healthCheck() async { + try { + await dio.get('/api/health'); + return true; + } catch (_) { + return false; + } + } +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..d774418 --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + static const _primaryColor = Color(0xFF6C63FF); + static const _accentColor = Color(0xFF00D9FF); + static const _backgroundColor = Color(0xFF0D1117); + static const _surfaceColor = Color(0xFF161B22); + static const _cardColor = Color(0xFF21262D); + + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: const ColorScheme.dark( + primary: _primaryColor, + secondary: _accentColor, + surface: _surfaceColor, + ), + scaffoldBackgroundColor: _backgroundColor, + cardColor: _cardColor, + textTheme: GoogleFonts.jetBrainsMonoTextTheme( + ThemeData.dark().textTheme, + ), + appBarTheme: const AppBarTheme( + backgroundColor: _surfaceColor, + elevation: 0, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: _primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + cardTheme: CardTheme( + color: _cardColor, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide(color: Colors.white.withAlpha(25)), + ), + ), + ); + } +} diff --git a/lib/core/utils/formatters.dart b/lib/core/utils/formatters.dart new file mode 100644 index 0000000..c1bde3a --- /dev/null +++ b/lib/core/utils/formatters.dart @@ -0,0 +1,31 @@ +import 'package:intl/intl.dart'; + +class Formatters { + /// Format a hash for display (first 8 + ... + last 8). + static String truncateHash(String hash, {int chars = 8}) { + if (hash.length <= chars * 2) return hash; + return '${hash.substring(0, chars)}...${hash.substring(hash.length - chars)}'; + } + + /// Format amount from smallest unit to display unit. + /// 50_000_000 -> "50.000000" + static String formatAmount(int amount, {int decimals = 6}) { + final value = amount / 1000000; + return value.toStringAsFixed(decimals); + } + + /// Format a timestamp string to readable date. + static String formatTimestamp(String timestamp) { + try { + final dt = DateTime.parse(timestamp); + return DateFormat('yyyy-MM-dd HH:mm:ss').format(dt.toLocal()); + } catch (_) { + return timestamp; + } + } + + /// Format large numbers with comma separators. + static String formatNumber(int number) { + return NumberFormat('#,###').format(number); + } +} diff --git a/lib/features/blocks/block_detail_screen.dart b/lib/features/blocks/block_detail_screen.dart new file mode 100644 index 0000000..c55a054 --- /dev/null +++ b/lib/features/blocks/block_detail_screen.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class BlockDetailScreen extends ConsumerWidget { + final String hash; + + const BlockDetailScreen({super.key, required this.hash}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Block Detail')), + body: Center( + child: Text( + 'Block Detail\n\nHash: $hash', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/blocks/blocks_screen.dart b/lib/features/blocks/blocks_screen.dart new file mode 100644 index 0000000..8e26fb8 --- /dev/null +++ b/lib/features/blocks/blocks_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class BlocksScreen extends ConsumerWidget { + const BlocksScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Block Explorer')), + body: const Center( + child: Text( + 'Block Explorer\n\nBrowse blocks with pagination.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/dashboard/dashboard_screen.dart b/lib/features/dashboard/dashboard_screen.dart new file mode 100644 index 0000000..cfa723b --- /dev/null +++ b/lib/features/dashboard/dashboard_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class DashboardScreen extends ConsumerWidget { + const DashboardScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Dashboard')), + body: const Center( + child: Text( + 'Blockchain Dashboard\n\nChain stats, recent blocks, and network overview.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/mining/mining_screen.dart b/lib/features/mining/mining_screen.dart new file mode 100644 index 0000000..0f0436b --- /dev/null +++ b/lib/features/mining/mining_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class MiningScreen extends ConsumerWidget { + const MiningScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Mining')), + body: const Center( + child: Text( + 'Mining\n\nMine blocks, view difficulty and rewards.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/transactions/send_tx_screen.dart b/lib/features/transactions/send_tx_screen.dart new file mode 100644 index 0000000..2b44138 --- /dev/null +++ b/lib/features/transactions/send_tx_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class SendTxScreen extends ConsumerWidget { + const SendTxScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Send Transaction')), + body: const Center( + child: Text( + 'Send Transaction\n\nForm: from, to, amount.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/transactions/transactions_screen.dart b/lib/features/transactions/transactions_screen.dart new file mode 100644 index 0000000..4dfbfba --- /dev/null +++ b/lib/features/transactions/transactions_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TransactionsScreen extends ConsumerWidget { + const TransactionsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Transactions')), + body: const Center( + child: Text( + 'Pending Transactions\n\nView mempool.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/features/wallet/wallet_screen.dart b/lib/features/wallet/wallet_screen.dart new file mode 100644 index 0000000..1df0a28 --- /dev/null +++ b/lib/features/wallet/wallet_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class WalletScreen extends ConsumerWidget { + const WalletScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Wallets')), + body: const Center( + child: Text( + 'Wallet Management\n\nCreate wallets, view balances.', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..acb07e8 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'core/theme/app_theme.dart'; +import 'routing/app_router.dart'; + +void main() { + runApp(const ProviderScope(child: BlockchainApp())); +} + +class BlockchainApp extends ConsumerWidget { + const BlockchainApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(appRouterProvider); + + return MaterialApp.router( + title: 'Blockchain Explorer', + theme: AppTheme.darkTheme, + routerConfig: router, + debugShowCheckedModeBanner: false, + ); + } +} diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart new file mode 100644 index 0000000..02afd3c --- /dev/null +++ b/lib/routing/app_router.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../features/dashboard/dashboard_screen.dart'; +import '../features/blocks/blocks_screen.dart'; +import '../features/blocks/block_detail_screen.dart'; +import '../features/wallet/wallet_screen.dart'; +import '../features/transactions/transactions_screen.dart'; +import '../features/transactions/send_tx_screen.dart'; +import '../features/mining/mining_screen.dart'; + +final appRouterProvider = Provider((ref) { + return GoRouter( + initialLocation: '/', + routes: [ + ShellRoute( + builder: (context, state, child) => ScaffoldWithNav(child: child), + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const DashboardScreen(), + ), + GoRoute( + path: '/blocks', + builder: (context, state) => const BlocksScreen(), + ), + GoRoute( + path: '/blocks/:hash', + builder: (context, state) => BlockDetailScreen( + hash: state.pathParameters['hash']!, + ), + ), + GoRoute( + path: '/wallets', + builder: (context, state) => const WalletScreen(), + ), + GoRoute( + path: '/transactions', + builder: (context, state) => const TransactionsScreen(), + ), + GoRoute( + path: '/transactions/send', + builder: (context, state) => const SendTxScreen(), + ), + GoRoute( + path: '/mining', + builder: (context, state) => const MiningScreen(), + ), + ], + ), + ], + ); +}); + +class ScaffoldWithNav extends StatelessWidget { + final Widget child; + const ScaffoldWithNav({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: child, + bottomNavigationBar: NavigationBar( + selectedIndex: _calculateIndex(GoRouterState.of(context).uri.path), + onDestinationSelected: (index) => _onTap(context, index), + destinations: const [ + NavigationDestination(icon: Icon(Icons.dashboard), label: 'Dashboard'), + NavigationDestination(icon: Icon(Icons.view_in_ar), label: 'Blocks'), + NavigationDestination(icon: Icon(Icons.account_balance_wallet), label: 'Wallets'), + NavigationDestination(icon: Icon(Icons.swap_horiz), label: 'Transactions'), + NavigationDestination(icon: Icon(Icons.hardware), label: 'Mining'), + ], + ), + ); + } + + int _calculateIndex(String path) { + if (path.startsWith('/blocks')) return 1; + if (path.startsWith('/wallets')) return 2; + if (path.startsWith('/transactions')) return 3; + if (path.startsWith('/mining')) return 4; + return 0; + } + + void _onTap(BuildContext context, int index) { + switch (index) { + case 0: context.go('/'); + case 1: context.go('/blocks'); + case 2: context.go('/wallets'); + case 3: context.go('/transactions'); + case 4: context.go('/mining'); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..ee473c6 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,34 @@ +name: blockchain_flutter +description: Flutter app for blockchain explorer, wallet management, and mining. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: ^3.6.0 + flutter: ">=3.27.0" + +dependencies: + flutter: + sdk: flutter + flutter_riverpod: ^2.6.1 + riverpod_annotation: ^2.6.1 + dio: ^5.7.0 + go_router: ^14.8.1 + freezed_annotation: ^2.4.4 + json_annotation: ^4.9.0 + flutter_secure_storage: ^9.2.4 + fl_chart: ^0.70.2 + intl: ^0.19.0 + google_fonts: ^6.2.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + build_runner: ^2.4.14 + freezed: ^2.5.7 + json_serializable: ^6.9.4 + riverpod_generator: ^2.6.3 + +flutter: + uses-material-design: true