From e7be97ed460e7ab56e02bb8bad7d7681bd604c0c Mon Sep 17 00:00:00 2001 From: Hanh Date: Tue, 14 Mar 2023 10:01:03 +1000 Subject: [PATCH] Customizable block explorer --- lib/coin/coin.dart | 5 +- lib/coin/ycash.dart | 2 +- lib/coin/zcash.dart | 7 +- lib/coin/zcashtest.dart | 2 +- lib/generated/intl/messages_en.dart | 1 + lib/generated/intl/messages_es.dart | 1 + lib/generated/intl/messages_fr.dart | 1 + lib/generated/l10n.dart | 10 +++ lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_es.arb | 3 +- lib/l10n/intl_fr.arb | 3 +- lib/main.dart | 3 + lib/settings.dart | 70 +++++++++++++++++++ lib/transaction.dart | 39 ++++++++--- native/zcash-sync | 2 +- packages/warp_api_ffi/lib/warp_api.dart | 9 +++ .../warp_api_ffi/lib/warp_api_generated.dart | 54 ++++++++++++++ pubspec.yaml | 2 +- 18 files changed, 195 insertions(+), 22 deletions(-) diff --git a/lib/coin/coin.dart b/lib/coin/coin.dart index 44eba62..c0f4df4 100644 --- a/lib/coin/coin.dart +++ b/lib/coin/coin.dart @@ -21,7 +21,6 @@ abstract class CoinBase { String get currency; String get ticker; int get coinIndex; - String get explorerUrl; AssetImage get image; String get dbName; late String dbDir; @@ -30,6 +29,7 @@ abstract class CoinBase { bool get supportsUA; bool get supportsMultisig; List get weights; + List get blockExplorers; void init(String dbDirPath) { dbDir = dbDirPath; @@ -70,8 +70,7 @@ abstract class CoinBase { await File(p.join(dbDir, dbName)).delete(); await File(p.join(dbDir, "$dbName-shm")).delete(); await File(p.join(dbDir, "$dbName-wal")).delete(); - } - catch (e) {} // ignore failure + } catch (e) {} // ignore failure } String _getFullPath(String dbPath) { diff --git a/lib/coin/ycash.dart b/lib/coin/ycash.dart index f24f41b..8e9b393 100644 --- a/lib/coin/ycash.dart +++ b/lib/coin/ycash.dart @@ -11,7 +11,6 @@ class YcashCoin extends CoinBase { int coinIndex = 347; String ticker = "YEC"; String dbName = "yec.db"; - String explorerUrl = "https://yecblockexplorer.com/tx/"; AssetImage image = AssetImage('assets/ycash.png'); List lwd = [ LWInstance("Lightwalletd", "https://lite.ycash.xyz:9067"), @@ -19,4 +18,5 @@ class YcashCoin extends CoinBase { bool supportsUA = false; bool supportsMultisig = true; List weights = [5, 25, 250]; + List blockExplorers = ["https://yecblockexplorer.com/tx"]; } diff --git a/lib/coin/zcash.dart b/lib/coin/zcash.dart index d2520d2..47e1ba8 100644 --- a/lib/coin/zcash.dart +++ b/lib/coin/zcash.dart @@ -11,7 +11,6 @@ class ZcashCoin extends CoinBase { int coinIndex = 133; String ticker = "ZEC"; String dbName = "zec.db"; - String explorerUrl = "https://explorer.zcha.in/transactions/"; AssetImage image = AssetImage('assets/zcash.png'); List lwd = [ LWInstance("Lightwalletd", "https://mainnet.lightwalletd.com:9067"), @@ -20,4 +19,10 @@ class ZcashCoin extends CoinBase { bool supportsUA = true; bool supportsMultisig = false; List weights = [0.05, 0.25, 2.50]; + List blockExplorers = [ + "https://explorer.zcha.in/transactions", + "https://blockchair.com/zcash/transaction", + "https://zcashblockexplorer.com/transactions", + "https://zecblockexplorer.com/tx" + ]; } diff --git a/lib/coin/zcashtest.dart b/lib/coin/zcashtest.dart index 43206f2..6d55117 100644 --- a/lib/coin/zcashtest.dart +++ b/lib/coin/zcashtest.dart @@ -11,7 +11,6 @@ class ZcashTestCoin extends CoinBase { int coinIndex = 133; String ticker = "ZEC"; String dbName = "zec-test.db"; - String explorerUrl = "https://explorer.zcha.in/transactions/"; AssetImage image = AssetImage('assets/zcash.png'); List lwd = [ LWInstance("Lightwalletd", "https://testnet.lightwalletd.com:9067"), @@ -19,4 +18,5 @@ class ZcashTestCoin extends CoinBase { bool supportsUA = true; bool supportsMultisig = false; List weights = [0.05, 0.25, 2.50]; + List blockExplorers = ["https://explorer.zcha.in/transactions"]; } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index f302a36..a104fb0 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -147,6 +147,7 @@ class MessageLookup extends MessageLookupByLibrary { "barcodeScannerIsNotAvailableOnDesktop": MessageLookupByLibrary.simpleMessage( "Barcode scanner is not available on desktop"), + "blockExplorer": MessageLookupByLibrary.simpleMessage("Block Explorer"), "blockReorgDetectedRewind": m3, "blue": MessageLookupByLibrary.simpleMessage("Blue"), "body": MessageLookupByLibrary.simpleMessage("Body"), diff --git a/lib/generated/intl/messages_es.dart b/lib/generated/intl/messages_es.dart index ad014f1..f1be15a 100644 --- a/lib/generated/intl/messages_es.dart +++ b/lib/generated/intl/messages_es.dart @@ -148,6 +148,7 @@ class MessageLookup extends MessageLookupByLibrary { "barcodeScannerIsNotAvailableOnDesktop": MessageLookupByLibrary.simpleMessage( "El escáner de código de barras no está disponible en el escritorio"), + "blockExplorer": MessageLookupByLibrary.simpleMessage("Block Explorer"), "blockReorgDetectedRewind": m3, "blue": MessageLookupByLibrary.simpleMessage("Azul"), "body": MessageLookupByLibrary.simpleMessage("Cuerpo"), diff --git a/lib/generated/intl/messages_fr.dart b/lib/generated/intl/messages_fr.dart index 520c90a..0d01935 100644 --- a/lib/generated/intl/messages_fr.dart +++ b/lib/generated/intl/messages_fr.dart @@ -148,6 +148,7 @@ class MessageLookup extends MessageLookupByLibrary { "barcodeScannerIsNotAvailableOnDesktop": MessageLookupByLibrary.simpleMessage( "Le Barcode scanner est seulement disponible sur mobile"), + "blockExplorer": MessageLookupByLibrary.simpleMessage("Block Explorer"), "blockReorgDetectedRewind": m3, "blue": MessageLookupByLibrary.simpleMessage("Bleu"), "body": MessageLookupByLibrary.simpleMessage("Contenu"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index d79af43..6b3da8b 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3141,6 +3141,16 @@ class S { args: [], ); } + + /// `Block Explorer` + String get blockExplorer { + return Intl.message( + 'Block Explorer', + name: 'blockExplorer', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1b9e63c..79f5b46 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -307,5 +307,6 @@ "never": "Never", "always": "Always", "scanTransparentAddresses": "Scan Transparent Addresses", - "scanningAddresses": "Scanning addresses" + "scanningAddresses": "Scanning addresses", + "blockExplorer": "Block Explorer" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 83c7de8..edebc93 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -305,5 +305,6 @@ "never": "Never", "always": "Always", "scanTransparentAddresses": "Scan Transparent Addresses", - "scanningAddresses": "Scanning addresses" + "scanningAddresses": "Scanning addresses", + "blockExplorer": "Block Explorer" } diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index ed7018a..2f2cfa2 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -306,5 +306,6 @@ "never": "Jamais", "always": "Toujours", "scanTransparentAddresses": "Scan Transparent Addresses", - "scanningAddresses": "Scanning addresses" + "scanningAddresses": "Scanning addresses", + "blockExplorer": "Block Explorer" } diff --git a/lib/main.dart b/lib/main.dart index a27a032..39764da 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -69,6 +69,7 @@ const MAXMONEY = 21000000; const DOC_URL = "https://hhanh00.github.io/zwallet/"; const APP_NAME = "YWallet"; const BACKUP_NAME = "$APP_NAME.age"; +const EXPLORER_KEY = "block_explorer"; const RECOVERY_FILE = "recover.bin"; @@ -414,6 +415,8 @@ class ZWalletAppState extends State { for (var c in coins) { _setProgress(0.2 * (c.coin + 1), 'Initializing ${c.ticker}'); await compute(_initWallet, {'coin': c, 'passwd': settings.dbPasswd}); + if (WarpApi.getProperty(c.coin, EXPLORER_KEY).isEmpty) + WarpApi.setProperty(c.coin, EXPLORER_KEY, c.blockExplorers[0]); } _setProgress(0.7, 'Restoring Active Account'); diff --git a/lib/settings.dart b/lib/settings.dart index 995a21b..473cae6 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,5 +1,6 @@ import 'package:YWallet/store.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:warp_api/warp_api.dart'; @@ -316,6 +317,20 @@ class SettingsState extends State name: 'memo', controller: _memoController, onSaved: _onMemo), + Row(children: [ + Expanded( + child: TextField( + controller: TextEditingController( + text: WarpApi.getProperty( + active.coin, EXPLORER_KEY)), + decoration: + InputDecoration(labelText: s.blockExplorer), + readOnly: true, + )), + IconButton( + onPressed: _editBlockExplorer, + icon: Icon(Icons.edit)) + ]), Padding(padding: EdgeInsets.symmetric(vertical: 8)), ButtonBar(children: confirmButtons(context, _onSave)) ])))))); @@ -427,6 +442,61 @@ class SettingsState extends State _editTheme() { Navigator.of(context).pushNamed('/edit_theme'); } + + _editBlockExplorer() async { + final selectedKey = 'selected_block_explorer'; + final customKey = 'custom_block_explorer'; + final s = S.of(context); + final customController = TextEditingController( + text: WarpApi.getProperty(active.coin, customKey)); + String selectedExplorer = WarpApi.getProperty(active.coin, selectedKey); + if (selectedExplorer.isEmpty) + selectedExplorer = active.coinDef.blockExplorers[0]; + + List> options = active.coinDef.blockExplorers + .map((explorer) => FormBuilderFieldOption( + child: Text(explorer), value: explorer)) + .toList(); + + options.add( + FormBuilderFieldOption(value: 'custom', child: Text(s.custom)), + ); + + final formKey = GlobalKey(); + + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(s.blockExplorer), + content: FormBuilder( + key: formKey, + child: Column(mainAxisSize: MainAxisSize.min, children: [ + FormBuilderRadioGroup( + orientation: OptionsOrientation.vertical, + name: 'block_explorers', + initialValue: selectedExplorer, + options: options), + FormBuilderTextField( + decoration: InputDecoration(labelText: s.custom), + name: 'custom', + controller: customController, + ), + ])), + actions: confirmButtons(context, () { + Navigator.of(context).pop(true); + }))) ?? + false; + if (confirmed) { + final state = formKey.currentState!; + final selection = state.fields['block_explorers']!.value; + final custom = state.fields['custom']!.value; + WarpApi.setProperty(active.coin, selectedKey, selection); + WarpApi.setProperty(active.coin, customKey, custom); + final url = selection == 'custom' ? custom : selection; + WarpApi.setProperty(active.coin, EXPLORER_KEY, url); + setState(() {}); + } + } } class ServerSelect extends StatefulWidget { diff --git a/lib/transaction.dart b/lib/transaction.dart index 293b0bf..3ef555f 100644 --- a/lib/transaction.dart +++ b/lib/transaction.dart @@ -2,6 +2,7 @@ import 'package:YWallet/contact.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:warp_api/data_fb_generated.dart'; +import 'package:warp_api/warp_api.dart'; import 'main.dart'; import 'settings.dart'; @@ -37,29 +38,45 @@ class TransactionState extends State { ListTile( title: Text('TXID'), subtitle: SelectableText('${tx.fullTxId}')), ListTile( - title: Text(S.of(context).height), subtitle: SelectableText('${tx.height}')), + title: Text(S.of(context).height), + subtitle: SelectableText('${tx.height}')), ListTile( - title: Text(S.of(context).confs), subtitle: SelectableText('${syncStatus.latestHeight - tx.height + 1}')), + title: Text(S.of(context).confs), + subtitle: + SelectableText('${syncStatus.latestHeight - tx.height + 1}')), ListTile( title: Text(S.of(context).timestamp), subtitle: Text('${tx.timestamp}')), - ListTile(title: Text(S.of(context).amount), subtitle: SelectableText(decimalFormat(tx.value, 8))), ListTile( - title: Text(S.of(context).address), subtitle: SelectableText('${tx.address}'), - trailing: IconButton(icon: Icon(Icons.contacts), onPressed: _addContact)), + title: Text(S.of(context).amount), + subtitle: SelectableText(decimalFormat(tx.value, 8))), ListTile( - title: Text(S.of(context).contactName), subtitle: SelectableText('${tx.contact ?? "N/A"}')), - ListTile(title: Text(S.of(context).memo), subtitle: SelectableText('${tx.memo}')), + title: Text(S.of(context).address), + subtitle: SelectableText('${tx.address}'), + trailing: IconButton( + icon: Icon(Icons.contacts), onPressed: _addContact)), + ListTile( + title: Text(S.of(context).contactName), + subtitle: SelectableText('${tx.contact ?? "N/A"}')), + ListTile( + title: Text(S.of(context).memo), + subtitle: SelectableText('${tx.memo}')), ButtonBar(alignment: MainAxisAlignment.center, children: [ - IconButton(onPressed: txIndex > 0 ? _prev : null, icon: Icon(Icons.chevron_left)), - ElevatedButton(onPressed: _onOpen, child: Text(S.of(context).openInExplorer)), - IconButton(onPressed: txIndex < n-1 ? _next : null, icon: Icon(Icons.chevron_right)), + IconButton( + onPressed: txIndex > 0 ? _prev : null, + icon: Icon(Icons.chevron_left)), + ElevatedButton( + onPressed: _onOpen, child: Text(S.of(context).openInExplorer)), + IconButton( + onPressed: txIndex < n - 1 ? _next : null, + icon: Icon(Icons.chevron_right)), ]), ])); } _onOpen() { - launchUrl(Uri.parse("${activeCoin().explorerUrl}${tx.fullTxId}")); + final url = WarpApi.getProperty(active.coin, EXPLORER_KEY); + launchUrl(Uri.parse("$url/${tx.fullTxId}")); } _prev() { diff --git a/native/zcash-sync b/native/zcash-sync index a2f6917..dbaa1c0 160000 --- a/native/zcash-sync +++ b/native/zcash-sync @@ -1 +1 @@ -Subproject commit a2f6917ce8de03ee6a4ec5a0564163d59f84da9e +Subproject commit dbaa1c02c93bbc8546c66e4478802528a6bb1256 diff --git a/packages/warp_api_ffi/lib/warp_api.dart b/packages/warp_api_ffi/lib/warp_api.dart index 58a2909..726ba64 100644 --- a/packages/warp_api_ffi/lib/warp_api.dart +++ b/packages/warp_api_ffi/lib/warp_api.dart @@ -611,6 +611,15 @@ class WarpApi { return unwrapResultBool( warp_api_lib.decrypt_db(toNative(dbPath), toNative(passwd))); } + + static String getProperty(int coin, String name) { + return unwrapResultString(warp_api_lib.get_property(coin, toNative(name))); + } + + static void setProperty(int coin, String name, String value) { + unwrapResultU8( + warp_api_lib.set_property(coin, toNative(name), toNative(value))); + } } String signOnlyIsolateFn(SignOnlyParams params) { diff --git a/packages/warp_api_ffi/lib/warp_api_generated.dart b/packages/warp_api_ffi/lib/warp_api_generated.dart index 4c6164e..a1ebbec 100644 --- a/packages/warp_api_ffi/lib/warp_api_generated.dart +++ b/packages/warp_api_ffi/lib/warp_api_generated.dart @@ -1350,6 +1350,38 @@ class NativeLibrary { late final _dart_clone_db_with_passwd _clone_db_with_passwd = _clone_db_with_passwd_ptr.asFunction<_dart_clone_db_with_passwd>(); + CResult_____c_char get_property( + int coin, + ffi.Pointer name, + ) { + return _get_property( + coin, + name, + ); + } + + late final _get_property_ptr = + _lookup>('get_property'); + late final _dart_get_property _get_property = + _get_property_ptr.asFunction<_dart_get_property>(); + + CResult_u8 set_property( + int coin, + ffi.Pointer name, + ffi.Pointer value, + ) { + return _set_property( + coin, + name, + value, + ); + } + + late final _set_property_ptr = + _lookup>('set_property'); + late final _dart_set_property _set_property = + _set_property_ptr.asFunction<_dart_set_property>(); + int has_cuda() { return _has_cuda(); } @@ -2467,6 +2499,28 @@ typedef _dart_clone_db_with_passwd = CResult_u8 Function( ffi.Pointer passwd, ); +typedef _c_get_property = CResult_____c_char Function( + ffi.Uint8 coin, + ffi.Pointer name, +); + +typedef _dart_get_property = CResult_____c_char Function( + int coin, + ffi.Pointer name, +); + +typedef _c_set_property = CResult_u8 Function( + ffi.Uint8 coin, + ffi.Pointer name, + ffi.Pointer value, +); + +typedef _dart_set_property = CResult_u8 Function( + int coin, + ffi.Pointer name, + ffi.Pointer value, +); + typedef _c_has_cuda = ffi.Int8 Function(); typedef _dart_has_cuda = int Function(); diff --git a/pubspec.yaml b/pubspec.yaml index eca133a..b3a150f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.3.6+411 +version: 1.3.6+412 environment: sdk: ">=2.12.0 <3.0.0"