Skip to content

Commit

Permalink
Implemented Blockstream Block Explorer Webview
Browse files Browse the repository at this point in the history
  • Loading branch information
aniketambore committed Feb 25, 2023
1 parent 661e023 commit ceb45a3
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 28 deletions.
24 changes: 11 additions & 13 deletions articles/00-Journey.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,29 @@

As a developer, I am always looking for new challenges and opportunities to learn. Recently, I decided to implement some of the projects defined in Advanced Topics and Next Steps in Chapter 14 of Programming Bitcoin by Jimmy Song. Specifically, I wanted to build a Bitcoin Testnet wallet as a mobile app using Flutter and the BDK library.

Before diving into coding, I wanted to make sure I had a strong understanding of the relevant standards and concepts. I reviewed BIP39, BIP32, BIP43, BIP44, and BIP84, and went through the complete documentation of BDK. I learned about Descriptors, which is a compact and semi-standard way to easily encode, or 'describe', how scripts (and subsequently, addresses) of a wallet should be generated. I also learned about PSBT (BIP:0174), which is how BDK signs the transaction and then broadcasts it while sending Bitcoins.
Before diving into coding, I wanted to make sure I had a strong understanding of the relevant standards and concepts. I reviewed [BIP39](https://bips.xyz/39), [BIP32](https://bips.xyz/32), [BIP43](https://bips.xyz/43), [BIP44](https://bips.xyz/44), and [BIP84](https://bips.xyz/84), and went through the complete documentation of [BDK](https://bitcoindevkit.org/getting-started/). I learned about [Descriptors](https://bitcoindevkit.org/descriptors/), which is a compact and semi-standard way to easily encode, or 'describe', how scripts (and subsequently, addresses) of a wallet should be generated. I also learned about [PSBT (BIP:0174)](https://bips.xyz/174), which is how BDK signs the transaction and then broadcasts it while sending Bitcoins.

Next, I had to decide on the right package to use for my project. I looked at 'bitcoin_flutter' but noticed some open issues on Github indicating that the package is no longer maintained. Instead, I decided to go with 'bdk_flutter', which utilizes the Rust BDK as its core library and uses Flutter Rust Bridge for binding.
Next, I had to decide on the right package to use for my project. I looked at '[bitcoin_flutter](https://github.com/dart-bitcoin/bitcoin_flutter)' but noticed some open issues on Github indicating that the package is no longer maintained. Instead, I decided to go with '[bdk_flutter](https://github.com/LtbLightning/bdk-flutter)', which utilizes the Rust BDK as its core library and uses Flutter Rust Bridge for binding.

To structure the project, I split the codebase into multiple local packages for the complete project, which helped enforce separation of concerns, promote cleaner APIs, and manage dependencies better. I used a mixed approach of feature-by-layer and feature-by-package, with each package having a src folder and barrel file.
To structure the project, I split the codebase into multiple local [packages](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages) for the complete project, which helped enforce separation of concerns, promote cleaner APIs, and manage dependencies better. I used a mixed approach of feature-by-layer and feature-by-package, with each package having a src folder and barrel file.

For data management, I followed the Repository pattern as there were two data sources (BDK and local cache) while getting the balance. I cached the wallet balance to improve user experience, allowing the app to respond faster. I had a different fetching policy while syncing the wallet and getting the balance.
For data management, I followed the [Repository](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/wallet_repository) pattern as there were two data sources (BDK and local cache) while getting the balance. I cached the wallet balance to improve user experience, allowing the app to respond faster. I had a different fetching policy while syncing the wallet and getting the balance.

For state management, I followed the BLoC design pattern using the flutter_bloc package from the bloc library, and used Cubit for most of the classes, which is actually a simplified version of a Bloc.
For state management, I followed the BLoC design pattern using the [flutter_bloc](https://pub.dev/packages/flutter_bloc) package from the bloc library, and used Cubit for most of the classes, which is actually a simplified version of a Bloc.

To handle routing in the app, I followed Navigator 2 wrapper package Routemaster, which handled all integration between the feature packages inside main.
To handle routing in the app, I followed Navigator 2 wrapper package [Routemaster](https://pub.dev/packages/routemaster), which handled all integration between the feature packages inside main.

From the start, I also focused on developing a common widget catalog for the project. Reusing already-created widgets saved a lot of time and effort when creating and maintaining the project. So I had a component library package.
From the start, I also focused on developing a common widget catalog for the project. Reusing already-created widgets saved a lot of time and effort when creating and maintaining the project. So I had a [component library package](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/component_library).

The app's architecture starts with the SplashScreen, which checks whether a wallet has already been created/loaded, and if not, launches the CreateWalletScreen.
The app's architecture starts with the [SplashScreen](https://github.com/aniketambore/savior-bitcoin-wallet/blob/main/lib/splash_screen.dart), which checks whether a wallet has already been created/loaded, and if not, launches the [CreateWalletScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/create_wallet).

The CreateWalletScreen displays two widgets: Create a New Wallet and Recover an Existing Wallet.

If a wallet already exists on the device, the HomeScreen is launched.
If a wallet already exists on the device, the [HomeScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/home) is launched.

This screen contains the home wallet screen with the Drawer (containing the AboutScreen and RecoveryPhraseScreen).
This screen contains the home wallet screen with the Drawer (containing the AboutScreen and [RecoveryPhraseScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/recover_phrase)).

The HomeScreen is simply a container for navigating between ReceiveAddressScreen, SendScreen, and TxHistoryScreen.
The HomeScreen is simply a container for navigating between [ReceiveAddressScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/receive), [SendScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/send), and [TxHistoryScreen](https://github.com/aniketambore/savior-bitcoin-wallet/tree/main/packages/features/tx_history).

I broke down the journey of building the wallet into 6 distinct steps:

Expand All @@ -43,10 +43,8 @@ The use of BIP39, BIP32, BIP43, BIP44 and BIP84 standards ensures that the walle

The architecture of the app is well-organized and follows the principles of separation of concerns, promoting cleaner APIs, and better dependency management. The app uses a feature-by-package and feature-by-layer approach, with local packages that promote modularity, scalability, and maintainability. The use of the Repository pattern allows for easy integration of multiple data sources, while the BLoC design pattern and Cubit simplify state management.


The app also uses a common widget catalog to promote code reusability, saving time and effort during development and maintenance. The use of Routemaster for navigation and integration between feature packages simplifies routing in the app, allowing for efficient navigation between screens.


Overall, the app provides a powerful and developer-friendly platform for building Bitcoin Testnet wallets. With its modular architecture, well-organized codebase, and intuitive interface, developers can easily build, test, and deploy Bitcoin wallets for both Android and iOS platforms. The use of industry-standard protocols and libraries ensures compatibility with other wallets and promotes security and reliability.

In conclusion, building a Bitcoin wallet is no easy task, but with the right tools and frameworks, developers can build robust and secure wallets that provide a seamless user experience. The use of Flutter, BDK, and other standards and protocols provides a powerful and reliable platform for building Bitcoin wallets. With the rise of Bitcoin, the need for secure and reliable wallets will only increase, and the development of apps like this is critical to the success of the Bitcoin ecosystem.
12 changes: 11 additions & 1 deletion lib/routing_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:send/send.dart';
import 'package:success_indicator/success_indicator.dart';
import 'package:tx_history/tx_history.dart';
import 'package:wallet_repository/wallet_repository.dart';
import 'package:webview/webview.dart';

Map<String, PageBuilder> buildRoutingTable({
required RoutemasterDelegate routerDelegate,
Expand Down Expand Up @@ -110,6 +111,15 @@ Map<String, PageBuilder> buildRoutingTable({
name: 'tx-history',
child: TxHistoryScreen(
walletRepository: walletRepository,
onTxSelected: (label, url, context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WebViewScreen(label: label, url: url),
fullscreenDialog: true,
),
);
},
),
);
},
Expand All @@ -130,7 +140,7 @@ Map<String, PageBuilder> buildRoutingTable({
},
),
);
}
},
};
}

Expand Down
76 changes: 62 additions & 14 deletions packages/features/tx_history/lib/src/tx_history_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,37 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tx_history/src/tx_history_cubit.dart';
import 'package:wallet_repository/wallet_repository.dart';

typedef TxSelected = Function(String label, String url, BuildContext context);

class TxHistoryScreen extends StatelessWidget {
const TxHistoryScreen({
super.key,
required this.walletRepository,
required this.onTxSelected,
});

final WalletRepository walletRepository;
final TxSelected onTxSelected;

@override
Widget build(BuildContext context) {
return BlocProvider<TxHistoryCubit>(
create: (_) => TxHistoryCubit(
walletRepository: walletRepository,
),
child: const TxHistoryView(),
child: TxHistoryView(
onTxSelected: onTxSelected,
),
);
}
}

class TxHistoryView extends StatelessWidget {
const TxHistoryView({super.key});
const TxHistoryView({
super.key,
required this.onTxSelected,
});
final TxSelected onTxSelected;

@override
Widget build(BuildContext context) {
Expand All @@ -48,7 +58,9 @@ class TxHistoryView extends StatelessWidget {
fontWeight: FontWeight.w800,
),
),
const _TxListCardHolder(),
_TxListCardHolder(
onTxSelected: onTxSelected,
),
ExpandedOutlinedButton(
label: 'Back to wallet',
onTap: () {
Expand All @@ -64,7 +76,10 @@ class TxHistoryView extends StatelessWidget {
}

class _TxListCardHolder extends StatelessWidget {
const _TxListCardHolder();
const _TxListCardHolder({
required this.onTxSelected,
});
final TxSelected onTxSelected;

@override
Widget build(BuildContext context) {
Expand All @@ -83,7 +98,10 @@ class _TxListCardHolder extends StatelessWidget {
builder: (context, state) {
return Expanded(
child: state is TxHistorySuccess
? _TxListCard(txList: state.txList)
? _TxListCard(
txList: state.txList,
onTxSelected: onTxSelected,
)
: state is TxHistoryFailure
? ExceptionIndicator(
onTryAgain: () {
Expand All @@ -101,9 +119,11 @@ class _TxListCardHolder extends StatelessWidget {
class _TxListCard extends StatelessWidget {
const _TxListCard({
required this.txList,
required this.onTxSelected,
});

final TxList txList;
final TxSelected onTxSelected;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -173,7 +193,10 @@ class _TxListCard extends StatelessWidget {
'No confirmed transactions',
style: TextStyle(fontSize: FontSize.medium),
)
: _TxList(txList: txList),
: _TxList(
txList: txList,
onTxSelected: onTxSelected,
),
),
],
),
Expand All @@ -185,23 +208,28 @@ class _TxListCard extends StatelessWidget {
class _TxList extends StatefulWidget {
const _TxList({
required this.txList,
required this.onTxSelected,
});

final TxList txList;
final TxSelected onTxSelected;

@override
State<_TxList> createState() => _TxListState();
}

class _TxListState extends State<_TxList> {
String getExplorerUrl(String txid) =>
'https://blockstream.info/testnet/tx/$txid';

@override
Widget build(BuildContext context) {
return ListView.separated(
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.txList.txList.length,
itemBuilder: (context, index) {
itemBuilder: (_, index) {
final tx = widget.txList.txList[index];
final timestamp = tx.confirmationTime?.timestamp?.d12() ?? 0;
final received = tx.received.toBTC();
Expand All @@ -210,14 +238,34 @@ class _TxListState extends State<_TxList> {
final height = tx.confirmationTime?.height ?? 0;
final txid = tx.txid;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
text('Timestamp: $timestamp', height),
text('Received: $received BTC', height),
text('Sent: $sent BTC', height),
text('Fees: $fees SATS', height),
text('Height: $height', height),
text('Txid: $txid', height),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
text('Timestamp: $timestamp', height),
text('Received: $received BTC', height),
text('Sent: $sent BTC', height),
text('Fees: $fees SATS', height),
text('Height: $height', height),
text('Txid: $txid', height),
],
),
Align(
alignment: Alignment.bottomRight,
child: IconButton(
icon: const Icon(
Icons.open_in_new_outlined,
size: 30,
),
onPressed: () {
widget.onTxSelected(
'Blockstream Explorer',
getExplorerUrl(txid),
context,
);
},
),
),
],
);
},
Expand Down
1 change: 1 addition & 0 deletions packages/features/webview/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
59 changes: 59 additions & 0 deletions packages/features/webview/lib/src/webview_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:component_library/component_library.dart';

class WebViewScreen extends StatefulWidget {
const WebViewScreen({
super.key,
required this.label,
required this.url,
});

final String label;
final String url;

@override
State<WebViewScreen> createState() => _WebViewScreenState();
}

class _WebViewScreenState extends State<WebViewScreen> {
late final WebViewController _controller;
bool _isLoading = true;

@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
if (progress == 100) setState(() => _isLoading = false);
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith(widget.url)) {
return NavigationDecision.navigate;
}
return NavigationDecision.prevent;
},
),
)
..loadRequest(
Uri.parse(widget.url),
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.label),
),
body: _isLoading
? const CenteredCircularProgressIndicator()
: WebViewWidget(
controller: _controller,
),
);
}
}
1 change: 1 addition & 0 deletions packages/features/webview/lib/webview.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/webview_screen.dart';
Loading

0 comments on commit ceb45a3

Please sign in to comment.