diff --git a/docs/building.md b/docs/building.md index 79e1bfb64..3d35acea2 100644 --- a/docs/building.md +++ b/docs/building.md @@ -116,6 +116,22 @@ cd .. or manually by creating the files referenced in that script with the specified content. ### Build plugins +#### Build script: `build_app.sh` +The `build_app.sh` script is use to build applications Stack Wallet. View the script's help message with `./build_app.sh -h` for more information on its usage. + +Options: + + - `a <app>`: Specify the application ID (required). Valid options are `stack_wallet` or `stack_duo`. + - `b <build_number>`: Specify the build number in 123 (required). + - `p <platform>`: Specify the platform to build for (required). Valid options are `android`, `ios`, `macos`, `linux`, or `windows`. + - `v <version>`: Specify the version of the application in 1.2.3 format (required). + - `i`: Optional flag to skip building crypto plugins. Useful for updating `pubspec.yaml` and white-labelling different apps with the same plugins. + +For example, +``` +./build_app.sh -a stack_wallet -p linux -v 2.1.0 -b 210 +``` + #### Building plugins for Android > Warning: This will take a long time, please be patient ``` diff --git a/lib/pages/buy_view/buy_order_details.dart b/lib/pages/buy_view/buy_order_details.dart index 4144a85ab..7021309a2 100644 --- a/lib/pages/buy_view/buy_order_details.dart +++ b/lib/pages/buy_view/buy_order_details.dart @@ -8,11 +8,15 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import '../../models/buy/response_objects/order.dart'; +import '../../notifications/show_flush_bar.dart'; import '../../themes/stack_colors.dart'; import '../../themes/theme_providers.dart'; import '../../utilities/assets.dart'; @@ -44,6 +48,16 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> { @override Widget build(BuildContext context) { + final orderDetails = ''' +Purchase ID: ${widget.order.paymentId} +User ID: ${widget.order.userId} +Quote ID: ${widget.order.quote.id} +Quoted cost: ${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()} +Quoted amount: ${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()} +Receiving ${widget.order.quote.crypto.ticker.toUpperCase()} address: ${widget.order.quote.receivingAddress} +Provider: Simplex +'''; + return ConditionalParent( condition: !isDesktop, builder: (child) { @@ -272,6 +286,43 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> { ), ], ), + const SizedBox(height: 8), + TextButton( + onPressed: () async { + await Clipboard.setData(ClipboardData(text: orderDetails)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 20, + height: 20, + color: Theme.of(context) + .extension<StackColors>()! + .buttonTextSecondary, + ), + const SizedBox( + width: 10, + ), + Text( + "Copy to clipboard", + style: STextStyles.desktopButtonSecondaryEnabled( + context, + ), + ), + ], + ), + ), const Spacer(), PrimaryButton( label: "Dismiss", diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index afb7c7a1d..a2b61c404 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -188,12 +188,14 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { _timeout = Duration.zero; _checkUseBiometrics(); + _pinTextController.addListener(_onPinChanged); super.initState(); } @override dispose() { // _shakeController.dispose(); + _pinTextController.removeListener(_onPinChanged); super.dispose(); } @@ -208,13 +210,27 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { ); } - final _pinTextController = TextEditingController(); final FocusNode _pinFocusNode = FocusNode(); late SecureStorageInterface _secureStore; late Biometrics biometrics; int pinCount = 1; + final _pinTextController = TextEditingController(); + + void _onPinChanged() async { + String enteredPin = _pinTextController.text; + final storedPin = await _secureStore.read(key: 'stack_pin'); + final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + + if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { + await Future<void>.delayed( + const Duration(milliseconds: 200), + ); + unawaited(_onUnlock()); + } + } + Widget get _body => Background( child: SafeArea( child: Scaffold( diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 57fa710ad..4c4bfa66c 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -61,9 +61,12 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { int pinCount = 1; + final TextEditingController _pinTextController = TextEditingController(); + @override void initState() { _secureStore = ref.read(secureStoreProvider); + _pinTextController.addListener(_onPinChanged); super.initState(); } @@ -74,9 +77,23 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { _pinPutController2.dispose(); _pinPutFocusNode1.dispose(); _pinPutFocusNode2.dispose(); + _pinTextController.removeListener(_onPinChanged); super.dispose(); } + void _onPinChanged() async { + String enteredPin = _pinTextController.text; + final storedPin = await _secureStore.read(key: 'stack_pin'); + final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + + if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { + await _pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.linear, + ); + } + } + @override Widget build(BuildContext context) { return Background( diff --git a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart index ff35130fe..e80374320 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/security_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/security_view.dart @@ -10,8 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../pinpad_views/lock_screen_view.dart'; -import 'change_pin_view/change_pin_view.dart'; + import '../../../../providers/global/prefs_provider.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -21,6 +20,8 @@ import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import '../../../pinpad_views/lock_screen_view.dart'; +import 'change_pin_view/change_pin_view.dart'; class SecurityView extends StatelessWidget { const SecurityView({ @@ -203,6 +204,54 @@ class SecurityView extends StatelessWidget { }, ), ), + // The "autoPin" preference (whether to automatically accept a correct PIN). + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Auto-accept correct PIN", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider + .select((value) => value.autoPin), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .autoPin = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), ], ), ), diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index abd236416..535f1310e 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -11,18 +11,19 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; +import 'package:uuid/uuid.dart'; + +import '../app_config.dart'; import '../db/hive/db.dart'; import '../services/event_bus/events/global/tor_status_changed_event.dart'; import '../services/event_bus/global_event_bus.dart'; -import '../app_config.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import 'amount/amount_unit.dart'; import 'constants.dart'; import 'enums/backup_frequency_type.dart'; import 'enums/languages_enum.dart'; import 'enums/sync_type_enum.dart'; -import '../wallets/crypto_currency/crypto_currency.dart'; -import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; -import 'package:uuid/uuid.dart'; class Prefs extends ChangeNotifier { Prefs._(); @@ -1103,4 +1104,30 @@ class Prefs extends ChangeNotifier { return actualMap; } + + // Automatic PIN entry. + + bool _autoPin = false; + + bool get autoPin => _autoPin; + + set autoPin(bool autoPin) { + if (_autoPin != autoPin) { + DB.instance.put<dynamic>( + boxName: DB.boxNamePrefs, + key: "autoPin", + value: autoPin, + ); + _autoPin = autoPin; + notifyListeners(); + } + } + + Future<bool> _getAutoPin() async { + return await DB.instance.get<dynamic>( + boxName: DB.boxNamePrefs, + key: "autoPin", + ) as bool? ?? + false; + } } diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index 5578d5fd0..2160671db 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:solana/solana.dart'; import '../networking/http.dart'; import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart'; @@ -15,6 +14,7 @@ import '../wallets/crypto_currency/crypto_currency.dart'; import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart'; import '../wallets/crypto_currency/intermediate/nano_currency.dart'; +import '../wallets/wallet/impl/solana_wallet.dart'; import 'connection_check/electrum_connection_check.dart'; import 'logger.dart'; import 'test_epic_box_connection.dart'; @@ -210,14 +210,20 @@ Future<bool> testNodeConnection({ case Solana(): try { - RpcClient rpcClient; - if (formData.host!.startsWith("http") || - formData.host!.startsWith("https")) { - rpcClient = RpcClient("${formData.host}:${formData.port}"); - } else { - rpcClient = RpcClient("http://${formData.host}:${formData.port}"); - } - await rpcClient.getEpochInfo().then((value) => testPassed = true); + final rpcClient = SolanaWallet.createRpcClient( + formData.host!, + formData.port!, + formData.useSSL ?? false, + ref.read(prefsChangeNotifierProvider), + ref.read(pTorService), + ); + + final health = await rpcClient.getHealth(); + Logging.instance.log( + "Solana testNodeConnection \"health=$health\"", + level: LogLevel.Info, + ); + return true; } catch (_) { testPassed = false; } diff --git a/lib/wallets/crypto_currency/coins/bitcoincash.dart b/lib/wallets/crypto_currency/coins/bitcoincash.dart index 5950fc913..9d9a291a8 100644 --- a/lib/wallets/crypto_currency/coins/bitcoincash.dart +++ b/lib/wallets/crypto_currency/coins/bitcoincash.dart @@ -55,8 +55,6 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override int get maxUnusedAddressGap => 50; - @override - int get maxNumberOfIndexesToCheck => 10000000; @override // change this to change the number of confirms a tx needs in order to show as confirmed diff --git a/lib/wallets/crypto_currency/coins/ecash.dart b/lib/wallets/crypto_currency/coins/ecash.dart index 102f509c8..ad1ec4f6c 100644 --- a/lib/wallets/crypto_currency/coins/ecash.dart +++ b/lib/wallets/crypto_currency/coins/ecash.dart @@ -50,8 +50,6 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface { @override int get maxUnusedAddressGap => 50; - @override - int get maxNumberOfIndexesToCheck => 10000000; @override // change this to change the number of confirms a tx needs in order to show as confirmed diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 1505abead..6cfc185e1 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -46,8 +46,7 @@ class Solana extends Bip39Currency { switch (network) { case CryptoCurrencyNetwork.main: return NodeModel( - host: - "https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one + host: "https://solana.stackwallet.com", port: 443, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(this), @@ -70,9 +69,13 @@ class Solana extends Bip39Currency { @override bool validateAddress(String address) { - return isPointOnEd25519Curve( - Ed25519HDPublicKey.fromBase58(address).toByteArray(), - ); + try { + return isPointOnEd25519Curve( + Ed25519HDPublicKey.fromBase58(address).toByteArray(), + ); + } catch (_) { + return false; + } } @override diff --git a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart index bbe5bd2ab..bb97b6754 100644 --- a/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/bip39_hd_currency.dart @@ -1,6 +1,7 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; + import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; @@ -16,7 +17,6 @@ abstract class Bip39HDCurrency extends Bip39Currency { List<DerivePathType> get supportedDerivationPathTypes; int get maxUnusedAddressGap => 50; - int get maxNumberOfIndexesToCheck => 10000; String constructDerivePath({ required DerivePathType derivePathType, diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 50ff0b31a..5cbfda157 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -18,6 +18,7 @@ import '../../../services/node_service.dart'; import '../../../services/tor_service.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../models/tx_data.dart'; import '../intermediate/bip39_wallet.dart'; @@ -245,14 +246,15 @@ class SolanaWallet extends Bip39Wallet<Solana> { } @override - Future<bool> pingCheck() { + Future<bool> pingCheck() async { + String? health; try { _checkClient(); - _rpcClient?.getHealth(); - return Future.value(true); + health = await _rpcClient?.getHealth(); + return health != null; } catch (e, s) { Logging.instance.log( - "$runtimeType Solana pingCheck failed: $e\n$s", + "$runtimeType Solana pingCheck failed \"health=$health\": $e\n$s", level: LogLevel.Error, ); return Future.value(false); @@ -453,32 +455,67 @@ class SolanaWallet extends Bip39Wallet<Solana> { } @override - Future<bool> updateUTXOs() { + Future<bool> updateUTXOs() async { // No UTXOs in Solana - return Future.value(false); + return false; } /// Make sure the Solana RpcClient uses Tor if it's enabled. /// - void _checkClient() async { + void _checkClient() { + final node = getCurrentNode(); + _rpcClient = createRpcClient( + node.host, + node.port, + node.useSSL, + prefs, + TorService.sharedInstance, + ); + } + + // static helper function for building a sol rpc client + static RpcClient createRpcClient( + final String host, + final int port, + final bool useSSL, + final Prefs prefs, + final TorService torService, + ) { HttpClient? httpClient; if (prefs.useTor) { // Make proxied HttpClient. - final ({InternetAddress host, int port}) proxyInfo = - TorService.sharedInstance.getProxyInfo(); + final proxyInfo = torService.getProxyInfo(); final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); httpClient = HttpClient(); SocksTCPClient.assignToHttpClient(httpClient, [proxySettings]); } - _rpcClient = RpcClient( - "${getCurrentNode().host}:${getCurrentNode().port}", + final regex = RegExp("^(http|https)://"); + + String editedHost; + if (host.startsWith(regex)) { + editedHost = host.replaceFirst(regex, ""); + } else { + editedHost = host; + } + + while (editedHost.endsWith("/")) { + editedHost = editedHost.substring(0, editedHost.length - 1); + } + + final uri = Uri( + scheme: useSSL ? "https" : "http", + host: editedHost, + port: port, + ); + + return RpcClient( + uri.toString(), timeout: const Duration(seconds: 30), customHeaders: {}, httpClient: httpClient, ); - return; } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index c7b8aa259..035ae649f 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -937,8 +937,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> int highestIndexWithHistory = 0; for (int index = 0; - index < cryptoCurrency.maxNumberOfIndexesToCheck && - gapCounter < cryptoCurrency.maxUnusedAddressGap; + gapCounter < cryptoCurrency.maxUnusedAddressGap; index += txCountBatchSize) { Logging.instance.log( "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", @@ -1017,10 +1016,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final List<Address> addressArray = []; int gapCounter = 0; int index = 0; - for (; - index < cryptoCurrency.maxNumberOfIndexesToCheck && - gapCounter < cryptoCurrency.maxUnusedAddressGap; - index++) { + for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) { Logging.instance.log( "index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter", level: LogLevel.Info,