Merge pull request from cypherstack/tor

Add Tor warning dialog and Network DevTools documentation for checking Tor usage outside of app
This commit is contained in:
Diego Salazar 2024-04-18 15:54:13 -06:00 committed by GitHub
commit a2f74ced8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 394 additions and 13 deletions

View file

@ -198,3 +198,11 @@ Run the following commands:
flutter pub get
flutter run -d windows
```
# Troubleshooting
Run with `-v` or `--verbose` to see a more detailed error. Certain exceptions (like missing a plugin library) may not report quality errors without `verbose`, especially on Windows.
## Tor
To test Tor usage, run Stack Wallet from Android Studio. Click the Flutter DevTools icon in the Run tab (next to the Hot Reload and Hot Restart buttons) and navigate to the Network tab. Connections using Tor will show as `GET InternetAddress('127.0.0.1', IPv4) 101 ws`. Connections outside of Tor will show the destination address directly.

View file

@ -10,7 +10,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -20,9 +19,11 @@ import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class HiddenSettings extends StatelessWidget {
@ -246,24 +247,23 @@ class HiddenSettings extends StatelessWidget {
}
},
),
const SizedBox(
height: 12,
),
Consumer(
builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
ref
.read(prefsChangeNotifierProvider)
.frostEnabled =
!(ref
.read(prefsChangeNotifierProvider)
.frostEnabled);
if (kDebugMode) {
print(
"FROST enabled: ${ref.read(prefsChangeNotifierProvider).frostEnabled}");
}
await showDialog<bool>(
context: context,
builder: (_) => TorWarningDialog(
coin: Coin.stellar,
),
);
},
child: RoundedWhiteContainer(
child: Text(
"Toggle FROST multisig",
"Show Tor warning popup",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!

View file

@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages/wallets_view/wallets_overview.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/supported_coins.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -26,6 +27,7 @@ import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class WalletListItem extends ConsumerWidget {
@ -58,6 +60,25 @@ class WalletListItem extends ConsumerWidget {
BorderRadius.circular(Constants.size.circularBorderRadius),
),
onPressed: () async {
// Check if Tor is enabled...
if (ref.read(prefsChangeNotifierProvider).useTor) {
// ... and if the coin supports Tor.
final cryptocurrency = SupportedCoins.coins[coin];
if (cryptocurrency != null && !cryptocurrency!.torSupport) {
// If not, show a Tor warning dialog.
final shouldContinue = await showDialog<bool>(
context: context,
builder: (_) => TorWarningDialog(
coin: coin,
),
) ??
false;
if (!shouldContinue) {
return;
}
}
}
if (walletCount == 1 && coin != Coin.ethereum) {
final wallet = ref
.read(pWallets)

View file

@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/wallets_view/wallets_overview.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/supported_coins.dart';
import 'package:stackwallet/themes/coin_icon_provider.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
@ -24,6 +25,7 @@ import 'package:stackwallet/wallets/isar/providers/all_wallets_info_provider.dar
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/dialogs/tor_warning_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
class WalletSummaryTable extends ConsumerStatefulWidget {
@ -85,7 +87,26 @@ class _DesktopWalletSummaryRowState
extends ConsumerState<DesktopWalletSummaryRow> {
bool _hovering = false;
void _onPressed() {
void _onPressed() async {
// Check if Tor is enabled...
if (ref.read(prefsChangeNotifierProvider).useTor) {
// ... and if the coin supports Tor.
final cryptocurrency = SupportedCoins.coins[widget.coin];
if (cryptocurrency != null && !cryptocurrency!.torSupport) {
// If not, show a Tor warning dialog.
final shouldContinue = await showDialog<bool>(
context: context,
builder: (_) => TorWarningDialog(
coin: widget.coin,
),
) ??
false;
if (!shouldContinue) {
return;
}
}
}
showDialog<void>(
context: context,
builder: (_) => DesktopDialog(

78
lib/supported_coins.dart Normal file
View file

@ -0,0 +1,78 @@
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
/// The supported coins.
class SupportedCoins {
/// A List of our supported coins.
static final List<CryptoCurrency> cryptocurrencies = [
// Mainnet coins.
Bitcoin(CryptoCurrencyNetwork.main),
Monero(CryptoCurrencyNetwork.main),
Banano(CryptoCurrencyNetwork.main),
Bitcoincash(CryptoCurrencyNetwork.main),
BitcoinFrost(CryptoCurrencyNetwork.main),
Dogecoin(CryptoCurrencyNetwork.main),
Ecash(CryptoCurrencyNetwork.main),
Epiccash(CryptoCurrencyNetwork.main),
Ethereum(CryptoCurrencyNetwork.main),
Firo(CryptoCurrencyNetwork.main),
Litecoin(CryptoCurrencyNetwork.main),
Namecoin(CryptoCurrencyNetwork.main),
Nano(CryptoCurrencyNetwork.main),
Particl(CryptoCurrencyNetwork.main),
Stellar(CryptoCurrencyNetwork.main),
Tezos(CryptoCurrencyNetwork.main),
Wownero(CryptoCurrencyNetwork.main),
/// Testnet coins.
Bitcoin(CryptoCurrencyNetwork.test),
Banano(CryptoCurrencyNetwork.test),
Bitcoincash(CryptoCurrencyNetwork.test),
BitcoinFrost(CryptoCurrencyNetwork.test),
Dogecoin(CryptoCurrencyNetwork.test),
Stellar(CryptoCurrencyNetwork.test),
Firo(CryptoCurrencyNetwork.test),
Litecoin(CryptoCurrencyNetwork.test),
Stellar(CryptoCurrencyNetwork.test),
];
/// A Map linking a CryptoCurrency with its associated Coin.
///
/// Temporary: Remove when the Coin enum is removed.
static final Map<Coin, CryptoCurrency> coins = {
Coin.bitcoin: Bitcoin(CryptoCurrencyNetwork.main),
Coin.monero: Monero(CryptoCurrencyNetwork.main),
Coin.banano: Banano(CryptoCurrencyNetwork.main),
Coin.bitcoincash: Bitcoincash(CryptoCurrencyNetwork.main),
Coin.bitcoinFrost: BitcoinFrost(CryptoCurrencyNetwork.main),
Coin.dogecoin: Dogecoin(CryptoCurrencyNetwork.main),
Coin.eCash: Ecash(CryptoCurrencyNetwork.main),
Coin.epicCash: Epiccash(CryptoCurrencyNetwork.main),
Coin.ethereum: Ethereum(CryptoCurrencyNetwork.main),
Coin.firo: Firo(CryptoCurrencyNetwork.main),
Coin.litecoin: Litecoin(CryptoCurrencyNetwork.main),
Coin.namecoin: Namecoin(CryptoCurrencyNetwork.main),
Coin.nano: Nano(CryptoCurrencyNetwork.main),
Coin.particl: Particl(CryptoCurrencyNetwork.main),
Coin.stellar: Stellar(CryptoCurrencyNetwork.main),
Coin.tezos: Tezos(CryptoCurrencyNetwork.main),
Coin.wownero: Wownero(CryptoCurrencyNetwork.main),
};
}

View file

@ -45,4 +45,12 @@ class Banano extends NanoCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Banano && other.network == network;
}
@override
int get hashCode => Object.hash(Banano, network);
}

View file

@ -25,6 +25,9 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface {
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 1;
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.bip44,
@ -200,4 +203,12 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Bitcoin && other.network == network;
}
@override
int get hashCode => Object.hash(Bitcoin, network);
}

View file

@ -23,6 +23,9 @@ class BitcoinFrost extends FrostCurrency {
@override
int get minConfirms => 1;
@override
bool get torSupport => true;
@override
NodeModel get defaultNode {
switch (network) {
@ -69,4 +72,12 @@ class BitcoinFrost extends FrostCurrency {
// TODO: implement validateAddress for frost addresses
return true;
}
@override
bool operator ==(Object other) {
return other is BitcoinFrost && other.network == network;
}
@override
int get hashCode => Object.hash(BitcoinFrost, network);
}

View file

@ -34,6 +34,9 @@ class Bitcoincash extends Bip39HDCurrency {
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 0; // bch zeroconf
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.bip44,
@ -286,4 +289,12 @@ class Bitcoincash extends Bip39HDCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Bitcoincash && other.network == network;
}
@override
int get hashCode => Object.hash(Bitcoincash, network);
}

View file

@ -20,6 +20,9 @@ class Dogecoin extends Bip39HDCurrency {
}
}
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.bip44,
@ -178,4 +181,12 @@ class Dogecoin extends Bip39HDCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Dogecoin && other.network == network;
}
@override
int get hashCode => Object.hash(Dogecoin, network);
}

View file

@ -32,6 +32,9 @@ class Ecash extends Bip39HDCurrency {
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 0; // bch zeroconf
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.eCash44,
@ -266,4 +269,12 @@ class Ecash extends Bip39HDCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Ecash && other.network == network;
}
@override
int get hashCode => Object.hash(Ecash, network);
}

View file

@ -61,4 +61,12 @@ class Epiccash extends Bip39Currency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Epiccash && other.network == network;
}
@override
int get hashCode => Object.hash(Epiccash, network);
}

View file

@ -34,4 +34,12 @@ class Ethereum extends Bip39Currency {
bool validateAddress(String address) {
return isValidEthereumAddress(address);
}
@override
bool operator ==(Object other) {
return other is Ethereum && other.network == network;
}
@override
int get hashCode => Object.hash(Ethereum, network);
}

View file

@ -24,6 +24,9 @@ class Firo extends Bip39HDCurrency {
@override
int get minConfirms => 1;
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.bip44,
@ -196,4 +199,12 @@ class Firo extends Bip39HDCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Firo && other.network == network;
}
@override
int get hashCode => Object.hash(Firo, network);
}

View file

@ -24,6 +24,9 @@ class Litecoin extends Bip39HDCurrency {
// change this to change the number of confirms a tx needs in order to show as confirmed
int get minConfirms => 1;
@override
bool get torSupport => true;
@override
List<DerivePathType> get supportedDerivationPathTypes => [
DerivePathType.bip44,
@ -209,4 +212,12 @@ class Litecoin extends Bip39HDCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Litecoin && other.network == network;
}
@override
int get hashCode => Object.hash(Litecoin, network);
}

View file

@ -44,4 +44,12 @@ class Monero extends CryptonoteCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Monero && other.network == network;
}
@override
int get hashCode => Object.hash(Monero, network);
}

View file

@ -22,6 +22,9 @@ class Namecoin extends Bip39HDCurrency {
// See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L58
int get minConfirms => 2;
@override
bool get torSupport => true;
@override
// See https://github.com/cypherstack/stack_wallet/blob/621aff47969761014e0a6c4e699cb637d5687ab3/lib/services/coins/namecoin/namecoin_wallet.dart#L80
String constructDerivePath({
@ -182,4 +185,12 @@ class Namecoin extends Bip39HDCurrency {
return false;
}
}
@override
bool operator ==(Object other) {
return other is Namecoin && other.network == network;
}
@override
int get hashCode => Object.hash(Namecoin, network);
}

View file

@ -45,4 +45,12 @@ class Nano extends NanoCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Nano && other.network == network;
}
@override
int get hashCode => Object.hash(Nano, network);
}

View file

@ -22,6 +22,9 @@ class Particl extends Bip39HDCurrency {
// See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L57
int get minConfirms => 1;
@override
bool get torSupport => true;
@override
// See https://github.com/cypherstack/stack_wallet/blob/d08b5c9b22b58db800ad07b2ceeb44c6d05f9cf3/lib/services/coins/particl/particl_wallet.dart#L68
String constructDerivePath(
@ -162,4 +165,12 @@ class Particl extends Bip39HDCurrency {
return false;
}
}
@override
bool operator ==(Object other) {
return other is Particl && other.network == network;
}
@override
int get hashCode => Object.hash(Particl, network);
}

View file

@ -39,4 +39,12 @@ class Stellar extends Bip39Currency {
@override
bool validateAddress(String address) =>
RegExp(r"^[G][A-Z0-9]{55}$").hasMatch(address);
@override
bool operator ==(Object other) {
return other is Stellar && other.network == network;
}
@override
int get hashCode => Object.hash(Stellar, network);
}

View file

@ -146,4 +146,12 @@ class Tezos extends Bip39Currency {
}
// ===========================================================================
@override
bool operator ==(Object other) {
return other is Tezos && other.network == network;
}
@override
int get hashCode => Object.hash(Tezos, network);
}

View file

@ -44,4 +44,12 @@ class Wownero extends CryptonoteCurrency {
throw UnimplementedError();
}
}
@override
bool operator ==(Object other) {
return other is Wownero && other.network == network;
}
@override
int get hashCode => Object.hash(Wownero, network);
}

View file

@ -20,6 +20,9 @@ abstract class CryptoCurrency {
// (used for eth currently)
bool get hasTokenSupport => false;
// Override in subclass if the currency has Tor support:
bool get torSupport => false;
// TODO: [prio=low] require these be overridden in concrete implementations to remove reliance on [coin]
int get fractionDigits => coin.decimals;
BigInt get satsPerCoin => Constants.satsPerCoin(coin);

View file

@ -26,6 +26,7 @@ class BasicDialog extends StatelessWidget {
this.desktopHeight = 474,
this.desktopWidth = 641,
this.canPopWithBackButton = false,
this.flex = false,
}) : super(key: key);
final Widget? leftButton;
@ -41,6 +42,8 @@ class BasicDialog extends StatelessWidget {
final bool canPopWithBackButton;
final bool flex;
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
@ -64,6 +67,10 @@ class BasicDialog extends StatelessWidget {
],
),
),
if (flex)
const Spacer(
flex: 2,
),
if (message != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
@ -72,6 +79,10 @@ class BasicDialog extends StatelessWidget {
style: STextStyles.desktopTextSmall(context),
),
),
if (flex)
const Spacer(
flex: 3,
),
if (leftButton != null || rightButton != null)
const SizedBox(
height: 32,

View file

@ -0,0 +1,44 @@
import 'package:flutter/cupertino.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/dialogs/basic_dialog.dart';
class TorWarningDialog extends StatelessWidget {
final Coin coin;
final VoidCallback? onContinue;
final VoidCallback? onCancel;
TorWarningDialog({
Key? key,
required this.coin,
this.onContinue,
this.onCancel,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BasicDialog(
title: "Warning! Tor not supported.",
message: "${coin.prettyName} is not compatible with Tor. "
"Continuing will leak your IP address."
"\n\nAre you sure you want to continue?",
// A PrimaryButton widget:
leftButton: PrimaryButton(
label: "Cancel",
onPressed: () {
onCancel?.call();
Navigator.of(context).pop(false);
},
),
rightButton: SecondaryButton(
label: "Continue",
onPressed: () {
onContinue?.call();
Navigator.of(context).pop(true);
},
),
flex: true,
);
}
}

View file

@ -30,6 +30,7 @@ import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/dialogs/basic_dialog.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart';
@ -94,6 +95,37 @@ class SimpleWalletCard extends ConsumerWidget {
final wallet = ref.read(pWallets).getWallet(walletId);
// If Tor enabled, show a warning if opening a wallet incompatible with Tor.
if (ref.read(prefsChangeNotifierProvider).useTor) {
if (!wallet.cryptoCurrency.torSupport) {
final shouldContinue = await showDialog<bool>(
context: context,
builder: (context) => BasicDialog(
title: "Warning! Tor not supported.",
message: "Stacky is not compatible with Tor."
"\n\nBy using it, you will leak your IP address. Are you sure you "
"want to continue?",
// A PrimaryButton widget:
leftButton: PrimaryButton(
label: "Cancel",
onPressed: () {
Navigator.of(context).pop(false);
},
),
rightButton: SecondaryButton(
label: "Continue",
onPressed: () {
Navigator.of(context).pop(true);
},
),
)) ??
false;
if (!shouldContinue) {
return;
}
}
}
if (context.mounted) {
final Future<void> loadFuture;
if (wallet is CwBasedInterface) {