From 0b48ba7eee7be9114f945a4b09115524d140272d Mon Sep 17 00:00:00 2001 From: kroplabeskidu <116884906+kroplabeskidu@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:06:57 +0100 Subject: [PATCH 1/3] Add Tor support for Ethereum web3dart was handed a plain http Client, so ETH and ERC-20 RPC could not use Tor and the coin was flagged as not Tor-ready. Give it an IOClient wrapping a SOCKS5-proxied HttpClient instead, the same way Solana does it, and do the same for the node connection test. Also add the usual node tor/clearnet mismatch check and set torSupport. --- lib/utilities/test_eth_node_connection.dart | 15 +- lib/utilities/test_node_connection.dart | 6 +- .../crypto_currency/coins/ethereum.dart | 3 + lib/wallets/wallet/impl/ethereum_wallet.dart | 54 ++++++- test/wallets/ethereum_tor_proxy_test.dart | 148 ++++++++++++++++++ 5 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 test/wallets/ethereum_tor_proxy_test.dart diff --git a/lib/utilities/test_eth_node_connection.dart b/lib/utilities/test_eth_node_connection.dart index be95a19ceb..e8c4473b99 100644 --- a/lib/utilities/test_eth_node_connection.dart +++ b/lib/utilities/test_eth_node_connection.dart @@ -1,12 +1,19 @@ -import 'package:http/http.dart'; -import 'package:web3dart/web3dart.dart' as web3; +import '../services/tor_service.dart'; +import '../wallets/wallet/impl/ethereum_wallet.dart'; +import 'prefs.dart'; -Future testEthNodeConnection(String host) async { - final web3.Web3Client client = web3.Web3Client(host, Client()); +Future testEthNodeConnection( + String host, { + required Prefs prefs, + required TorService torService, +}) async { + final client = EthereumWallet.createWeb3Client(host, prefs, torService); try { await client.getBlockNumber(); return true; } catch (_) { return false; + } finally { + await client.dispose(); } } diff --git a/lib/utilities/test_node_connection.dart b/lib/utilities/test_node_connection.dart index 0b3504ce21..80068fce59 100644 --- a/lib/utilities/test_node_connection.dart +++ b/lib/utilities/test_node_connection.dart @@ -235,7 +235,11 @@ Future testNodeConnection({ case Ethereum(): try { - testPassed = await testEthNodeConnection(formData.host!); + testPassed = await testEthNodeConnection( + formData.host!, + prefs: read(prefsChangeNotifierProvider), + torService: read(pTorService), + ); } catch (_) { testPassed = false; } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index 448f4c9ff9..57b79a2be4 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -47,6 +47,9 @@ class Ethereum extends Bip39Currency { @override bool get hasTokenSupport => true; + @override + bool get torSupport => true; + @override NodeModel defaultNode({required bool isPrimary}) => NodeModel( host: "https://eth2.stackwallet.com", diff --git a/lib/wallets/wallet/impl/ethereum_wallet.dart b/lib/wallets/wallet/impl/ethereum_wallet.dart index f5444c7428..e2b0674509 100644 --- a/lib/wallets/wallet/impl/ethereum_wallet.dart +++ b/lib/wallets/wallet/impl/ethereum_wallet.dart @@ -1,16 +1,20 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart'; +import 'package:http/io_client.dart'; import 'package:isar_community/isar.dart'; +import 'package:socks5_proxy/socks.dart'; import 'package:wallet/wallet.dart' as eth_wallet; import 'package:web3dart/json_rpc.dart' show RPCError; import 'package:web3dart/web3dart.dart' as web3; +import '../../../app_config.dart'; import '../../../dto/ethereum/eth_tx_dto.dart'; +import '../../../exceptions/wallet/node_tor_mismatch_config_exception.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; @@ -21,9 +25,12 @@ import '../../../models/paymint/fee_object_model.dart'; import '../../../services/ethereum/ethereum_api.dart'; import '../../../services/event_bus/events/global/updated_in_background_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/tor_service.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/eth_commons.dart'; import '../../../utilities/logger.dart'; +import '../../../utilities/prefs.dart'; +import '../../../utilities/tor_plain_net_option_enum.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../models/tx_data.dart'; import '../intermediate/bip39_wallet.dart'; @@ -85,8 +92,6 @@ Future> findReplacedPendingEthereumTransactions({ return replacedTransactions; } -// Eth can not use tor with web3dart - @visibleForTesting ({BigInt maxFeePerGas, BigInt maxPriorityFeePerGas}) resolveEip1559FeeCaps({ required BigInt baseFee, @@ -144,10 +149,45 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { web3.Web3Client getEthClient() { final node = getCurrentNode(); - // Eth can not use tor with web3dart as Client does not support proxies - final client = Client(); + final netOption = TorPlainNetworkOption.fromNodeData( + node.torEnabled, + node.clearnetEnabled, + ); - return web3.Web3Client(node.host, client); + if (prefs.useTor) { + if (netOption == TorPlainNetworkOption.clear) { + throw NodeTorMismatchConfigException( + message: "TOR enabled but node set to clearnet only", + ); + } + } else { + if (netOption == TorPlainNetworkOption.tor) { + throw NodeTorMismatchConfigException( + message: "TOR off but node set to TOR only", + ); + } + } + + return createWeb3Client(node.host, prefs, TorService.sharedInstance); + } + + // static helper function for building a web3 client, proxied over tor if + // enabled + static web3.Web3Client createWeb3Client( + String url, + Prefs prefs, + TorService torService, + ) { + final httpClient = HttpClient(); + + if (AppConfig.hasFeature(AppFeature.tor) && prefs.useTor) { + final proxyInfo = torService.getProxyInfo(); + SocksTCPClient.assignToHttpClient(httpClient, [ + ProxySettings(proxyInfo.host, proxyInfo.port), + ]); + } + + return web3.Web3Client(url, IOClient(httpClient)); } Amount estimateEthFee(BigInt feeRate, int gasLimit, int decimals) { @@ -292,8 +332,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { @override Future pingCheck() async { - final web3.Web3Client client = getEthClient(); try { + final client = getEthClient(); await client.getBlockNumber(); return true; } catch (_) { diff --git a/test/wallets/ethereum_tor_proxy_test.dart b/test/wallets/ethereum_tor_proxy_test.dart new file mode 100644 index 0000000000..a532408c8b --- /dev/null +++ b/test/wallets/ethereum_tor_proxy_test.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:stackwallet/services/tor_service.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/wallets/wallet/impl/ethereum_wallet.dart'; + +class _MockPrefs extends Mock implements Prefs { + _MockPrefs(this._useTor); + + final bool _useTor; + + @override + bool get useTor => _useTor; +} + +class _FakeTorService extends Fake implements TorService { + _FakeTorService(this.proxyInfo); + + final ({InternetAddress host, int port}) proxyInfo; + int proxyInfoReads = 0; + + @override + ({InternetAddress host, int port}) getProxyInfo() { + proxyInfoReads++; + return proxyInfo; + } +} + +const _rpcBody = '{"jsonrpc":"2.0","id":0,"result":"0x2a"}'; + +String _rpcHttpResponse() => + 'HTTP/1.1 200 OK\r\n' + 'Content-Type: application/json\r\n' + 'Content-Length: ${_rpcBody.length}\r\n' + 'Connection: close\r\n\r\n' + '$_rpcBody'; + +// Minimal SOCKS5 server that records the CONNECT target and answers the +// tunnelled HTTP request with a canned JSON-RPC eth_blockNumber result. +class _FakeSocksServer { + late final ServerSocket _server; + int? addressType; + String? target; + int? port; + final request = StringBuffer(); + + int get listeningPort => _server.port; + + Future start() async { + _server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + _server.listen((socket) { + var stage = 0; + socket.listen((Uint8List bytes) { + switch (stage) { + case 0: + socket.add([0x05, 0x00]); + stage = 1; + case 1: + addressType = bytes[3]; + if (addressType == 0x03) { + target = String.fromCharCodes(bytes.sublist(5, 5 + bytes[4])); + } else { + target = bytes.sublist(4, bytes.length - 2).join('.'); + } + port = (bytes[bytes.length - 2] << 8) | bytes[bytes.length - 1]; + socket.add([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]); + stage = 2; + case 2: + request.write(utf8.decode(bytes, allowMalformed: true)); + socket.write(_rpcHttpResponse()); + socket.close(); + stage = 3; + default: + request.write(utf8.decode(bytes, allowMalformed: true)); + } + }); + }); + } + + Future stop() => _server.close(); +} + +void main() { + late _FakeSocksServer socks; + + setUp(() async { + socks = _FakeSocksServer(); + await socks.start(); + }); + + tearDown(() => socks.stop()); + + test('web3 client sends RPC through the Tor SOCKS5 proxy', () async { + final torService = _FakeTorService(( + host: InternetAddress.loopbackIPv4, + port: socks.listeningPort, + )); + final client = EthereumWallet.createWeb3Client( + 'http://eth.example.invalid:8545', + _MockPrefs(true), + torService, + ); + + final height = await client.getBlockNumber(); + await client.dispose(); + + expect(height, 42); + expect(torService.proxyInfoReads, 1); + // Hostname goes to the proxy, so there is no local DNS lookup. + expect(socks.addressType, 0x03); + expect(socks.target, 'eth.example.invalid'); + expect(socks.port, 8545); + expect(socks.request.toString(), contains('eth_blockNumber')); + }); + + test('web3 client connects directly when Tor is off', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + server.listen((req) async { + await utf8.decoder.bind(req).join(); + req.response + ..headers.contentType = ContentType.json + ..write(_rpcBody); + await req.response.close(); + }); + + final torService = _FakeTorService(( + host: InternetAddress.loopbackIPv4, + port: socks.listeningPort, + )); + final client = EthereumWallet.createWeb3Client( + 'http://127.0.0.1:${server.port}', + _MockPrefs(false), + torService, + ); + + final height = await client.getBlockNumber(); + await client.dispose(); + await server.close(force: true); + + expect(height, 42); + expect(torService.proxyInfoReads, 0); + expect(socks.target, isNull); + }); +} From 2b6b4f6d404e5c9a5ef85dca7e340c66765e7b7a Mon Sep 17 00:00:00 2001 From: kroplabeskidu <116884906+kroplabeskidu@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:06:57 +0100 Subject: [PATCH 2/3] Mark Nano, Banano and Fact0rn as Tor-ready, proxy Stellar node test Nano and Banano already send every request through Tor, and Fact0rn uses the regular ElectrumX client, so they only needed the torSupport flag. The Stellar node test proxied its first request but then created a StellarSDK with the default client, so the follow-up ledger query went out directly. --- lib/utilities/test_stellar_node_connection.dart | 12 +++++++++++- lib/wallets/crypto_currency/coins/fact0rn.dart | 4 ++-- .../crypto_currency/intermediate/nano_currency.dart | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/utilities/test_stellar_node_connection.dart b/lib/utilities/test_stellar_node_connection.dart index 3af380f633..b61f7a3c7c 100644 --- a/lib/utilities/test_stellar_node_connection.dart +++ b/lib/utilities/test_stellar_node_connection.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:socks5_proxy/socks.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; import '../app_config.dart'; @@ -28,7 +30,15 @@ Future testStellarNodeConnection(String host, int port) async { if (response.code == 200) { //Get chain height for sdk - final StellarSDK stellarSdk = StellarSDK(host); + HttpClient? httpClient; + if (AppConfig.hasFeature(AppFeature.tor) && Prefs.instance.useTor) { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + httpClient = HttpClient(); + SocksTCPClient.assignToHttpClient(httpClient, [ + ProxySettings(proxyInfo.host, proxyInfo.port), + ]); + } + final StellarSDK stellarSdk = StellarSDK(host, httpClient: httpClient); final height = await stellarSdk.ledgers .order(RequestBuilderOrder.DESC) .limit(1) diff --git a/lib/wallets/crypto_currency/coins/fact0rn.dart b/lib/wallets/crypto_currency/coins/fact0rn.dart index 1ef113ac04..b6687c8ac8 100644 --- a/lib/wallets/crypto_currency/coins/fact0rn.dart +++ b/lib/wallets/crypto_currency/coins/fact0rn.dart @@ -48,7 +48,7 @@ class Fact0rn extends Bip39HDCurrency with ElectrumXCurrencyInterface { String get ticker => _ticker; @override - bool get torSupport => false; + bool get torSupport => true; @override List get supportedDerivationPathTypes => [ @@ -184,7 +184,7 @@ class Fact0rn extends Bip39HDCurrency with ElectrumXCurrencyInterface { coinName: identifier, isFailover: true, isDown: false, - torEnabled: false, + torEnabled: true, clearnetEnabled: true, isPrimary: isPrimary, ); diff --git a/lib/wallets/crypto_currency/intermediate/nano_currency.dart b/lib/wallets/crypto_currency/intermediate/nano_currency.dart index a553a6d6c9..573e91d7c0 100644 --- a/lib/wallets/crypto_currency/intermediate/nano_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/nano_currency.dart @@ -18,6 +18,9 @@ abstract class NanoCurrency extends Bip39Currency { @override bool get hasBuySupport => false; + @override + bool get torSupport => true; + @override int get defaultSeedPhraseLength => 24; From 0f421bc06703296dc86bb438921c5590e2f0cee9 Mon Sep 17 00:00:00 2001 From: kroplabeskidu <116884906+kroplabeskidu@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:06:57 +0100 Subject: [PATCH 3/3] Load remote images over Tor when it is enabled Image.network and SvgPicture.network use their own HTTP clients and ignore the Tor setting, so token logos, exchange and coin logos, CakePay card images, ShopinBit attachments and theme previews were fetched directly even in Tor mode. Add TorAwareNetworkImage and TorAwareSvgNetworkLoader, which go through the app's HTTP wrapper, and use them at all call sites. --- lib/networking/tor_aware_network_image.dart | 123 ++++++++++ .../sub_widgets/add_token_list_element.dart | 5 +- .../sub_widgets/coin_select_item.dart | 9 +- .../cakepay/cakepay_card_detail_view.dart | 5 +- lib/pages/cakepay/cakepay_vendors_view.dart | 49 ++-- .../exchange_currency_selection_view.dart | 230 +++++++++--------- .../sub_widgets/exchange_provider_option.dart | 37 +-- .../sub_widgets/stack_theme_card.dart | 114 +++++---- .../shopinbit/shopinbit_ticket_detail.dart | 45 ++-- lib/widgets/icon_widgets/eth_token_icon.dart | 5 +- lib/widgets/icon_widgets/sol_token_icon.dart | 5 +- .../textfields/exchange_textfield.dart | 7 +- .../wallet_info_row_coin_icon.dart | 40 +-- 13 files changed, 401 insertions(+), 273 deletions(-) create mode 100644 lib/networking/tor_aware_network_image.dart diff --git a/lib/networking/tor_aware_network_image.dart b/lib/networking/tor_aware_network_image.dart new file mode 100644 index 0000000000..ef4cb03eaa --- /dev/null +++ b/lib/networking/tor_aware_network_image.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../app_config.dart'; +import '../services/tor_service.dart'; +import '../utilities/prefs.dart'; +import 'http.dart'; + +/// Fetches [url] through the app's [HTTP] client, routing over Tor when it is +/// enabled. Throws if Tor is enabled but not connected, so nothing falls back +/// to clearnet. +Future fetchTorAwareBytes(String url) async { + final response = await const HTTP().get( + url: Uri.parse(url), + proxyInfo: !AppConfig.hasFeature(AppFeature.tor) + ? null + : Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, + ); + + if (response.code != 200) { + throw Exception("Failed to load $url: status=${response.code}"); + } + + return Uint8List.fromList(response.bodyBytes); +} + +/// Drop-in replacement for [NetworkImage] that respects the Tor preference. +/// Use with [Image] instead of [Image.network], which always goes clearnet. +@immutable +class TorAwareNetworkImage extends ImageProvider { + const TorAwareNetworkImage(this.url); + + final String url; + + @override + Future obtainKey(ImageConfiguration configuration) => + SynchronousFuture(this); + + @override + ImageStreamCompleter loadImage( + TorAwareNetworkImage key, + ImageDecoderCallback decode, + ) { + final chunkEvents = StreamController(); + return MultiFrameImageStreamCompleter( + codec: _load(key, decode, chunkEvents), + chunkEvents: chunkEvents.stream, + scale: 1.0, + debugLabel: key.url, + ); + } + + Future _load( + TorAwareNetworkImage key, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async { + try { + // Lets loadingBuilder show its indicator while the request runs. + chunkEvents.add( + const ImageChunkEvent( + cumulativeBytesLoaded: 0, + expectedTotalBytes: null, + ), + ); + final bytes = await fetchTorAwareBytes(key.url); + final buffer = await ui.ImmutableBuffer.fromUint8List(bytes); + return await decode(buffer); + } catch (_) { + // Don't keep a failed load in the cache. + scheduleMicrotask(() { + PaintingBinding.instance.imageCache.evict(key); + }); + rethrow; + } finally { + unawaited(chunkEvents.close()); + } + } + + @override + bool operator ==(Object other) => + other is TorAwareNetworkImage && other.url == url; + + @override + int get hashCode => url.hashCode; + + @override + String toString() => + "${objectRuntimeType(this, "TorAwareNetworkImage")}" + "(\"$url\")"; +} + +/// Drop-in replacement for the loader behind [SvgPicture.network] that +/// respects the Tor preference. +/// Use as `SvgPicture(TorAwareSvgNetworkLoader(url))`. +@immutable +class TorAwareSvgNetworkLoader extends SvgLoader { + const TorAwareSvgNetworkLoader(this.url); + + final String url; + + @override + Future prepareMessage(BuildContext? context) => + fetchTorAwareBytes(url); + + @override + String provideSvg(Uint8List? message) => + utf8.decode(message!, allowMalformed: true); + + @override + bool operator ==(Object other) => + other is TorAwareSvgNetworkLoader && other.url == url; + + @override + int get hashCode => url.hashCode; +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index 65088fbc94..a2b327b563 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -16,6 +16,7 @@ import 'package:isar_community/isar.dart'; import '../../../../models/isar/exchange_cache/currency.dart'; import '../../../../models/isar/models/contract.dart'; import '../../../../models/isar/models/ethereum/eth_contract.dart'; +import '../../../../networking/tor_aware_network_image.dart'; import '../../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../../services/exchange/exchange_data_loading_service.dart'; import '../../../../themes/stack_colors.dart'; @@ -101,8 +102,8 @@ class _AddTokenListElementState extends ConsumerState { ) else currency != null - ? SvgPicture.network( - currency!.image, + ? SvgPicture( + TorAwareSvgNetworkLoader(currency!.image), width: iconSize, height: iconSize, placeholderBuilder: (_) => diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 95f3f4a790..2ac222b7c7 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -19,6 +19,7 @@ import '../../../../models/add_wallet_list_entity/add_wallet_list_entity.dart'; import '../../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import '../../../../models/add_wallet_list_entity/sub_classes/sol_token_entity.dart'; import '../../../../models/isar/exchange_cache/currency.dart'; +import '../../../../networking/tor_aware_network_image.dart'; import '../../../../providers/providers.dart'; import '../../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../../services/exchange/exchange_data_loading_service.dart'; @@ -146,15 +147,15 @@ class _CoinSelectItemState extends ConsumerState { else tokenImageUri != null ? tokenImageUri!.toLowerCase().endsWith(".svg") - ? SvgPicture.network( - tokenImageUri!, + ? SvgPicture( + TorAwareSvgNetworkLoader(tokenImageUri!), width: 26, height: 26, placeholderBuilder: (_) => const AppIcon(width: 26, height: 26), ) - : Image.network( - tokenImageUri!, + : Image( + image: TorAwareNetworkImage(tokenImageUri!), width: 26, height: 26, errorBuilder: (_, _, _) => SvgPicture.file( diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart index c53633ef21..eec53dc0db 100644 --- a/lib/pages/cakepay/cakepay_card_detail_view.dart +++ b/lib/pages/cakepay/cakepay_card_detail_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../providers/global/locale_provider.dart'; import '../../services/cakepay/cakepay_service.dart'; import '../../services/cakepay/src/models/card.dart'; @@ -340,8 +341,8 @@ class _CardImage extends StatelessWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image.network( - imageUrl, + child: Image( + image: TorAwareNetworkImage(imageUrl), width: isDesktop ? 300 : null, fit: isDesktop ? .contain : .fitWidth, loadingBuilder: (_, child, event) { diff --git a/lib/pages/cakepay/cakepay_vendors_view.dart b/lib/pages/cakepay/cakepay_vendors_view.dart index 59b145825b..c3c0dbc13a 100644 --- a/lib/pages/cakepay/cakepay_vendors_view.dart +++ b/lib/pages/cakepay/cakepay_vendors_view.dart @@ -2,6 +2,7 @@ import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../services/cakepay/cakepay_service.dart'; import '../../services/cakepay/src/models/card.dart'; import '../../themes/stack_colors.dart'; @@ -87,9 +88,8 @@ class _CakePayVendorsViewState extends State { } Future _onCardTapped(CakePayCard card) async { - await Navigator.of( - context, - ).pushNamed(CakePayCardDetailView.routeName, arguments: card); + await Navigator.of(context) + .pushNamed(CakePayCardDetailView.routeName, arguments: card); } @override @@ -130,9 +130,9 @@ class _CakePayVendorsViewState extends State { condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () => Navigator.of(context).pop(), @@ -210,9 +210,9 @@ class _CakePayVendorsViewState extends State { child: Text( "No items", style: STextStyles.w500_14(context).copyWith( - color: Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of(context) + .extension()! + .textSubtitle1, ), ), ), @@ -226,9 +226,9 @@ class _CakePayVendorsViewState extends State { Text( error.toString(), style: STextStyles.w500_14(context).copyWith( - color: Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of(context) + .extension()! + .textSubtitle1, ), ), const SizedBox(height: 16), @@ -276,9 +276,9 @@ class _SearchField extends StatelessWidget { focusNode: focusNode, style: isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of( - context, - ).extension()!.textFieldActiveText, + color: Theme.of(context) + .extension()! + .textFieldActiveText, ) : STextStyles.field(context), decoration: @@ -327,9 +327,8 @@ class _CountryDropdown extends StatelessWidget { ); final itemStyle = isDesktop - ? STextStyles.desktopTextExtraSmall( - context, - ).copyWith(color: colors.textFieldActiveText) + ? STextStyles.desktopTextExtraSmall(context) + .copyWith(color: colors.textFieldActiveText) : STextStyles.w500_14(context); return ClipRRect( @@ -341,9 +340,8 @@ class _CountryDropdown extends StatelessWidget { hint: Text( "All countries", style: isDesktop - ? STextStyles.desktopTextExtraSmall( - context, - ).copyWith(color: colors.textFieldDefaultSearchIconLeft) + ? STextStyles.desktopTextExtraSmall(context) + .copyWith(color: colors.textFieldDefaultSearchIconLeft) : STextStyles.fieldLabel(context), ), items: [ @@ -446,8 +444,8 @@ class _CardTile extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(4), child: card.cardImageUrl != null - ? Image.network( - card.cardImageUrl!, + ? Image( + image: TorAwareNetworkImage(card.cardImageUrl!), width: isDesktop ? 60 : 48, height: isDesktop ? 40 : 32, fit: BoxFit.cover, @@ -483,9 +481,8 @@ class _CardTile extends StatelessWidget { ].join(' '), style: isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle12( - context, - ).copyWith(color: colors.textSubtitle1), + : STextStyles.itemSubtitle12(context) + .copyWith(color: colors.textSubtitle1), ), ], ), diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index 4a539ec9d4..b5ebfc9b34 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -19,6 +19,7 @@ import '../../../app_config.dart'; import '../../../models/exchange/aggregate_currency.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; +import '../../../networking/tor_aware_network_image.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; import '../../../themes/stack_colors.dart'; @@ -73,19 +74,19 @@ class _ExchangeCurrencySelectionViewState showDialog( context: context, barrierDismissible: false, - builder: - (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of( - context, - ).extension()!.overlay.withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Loading currencies", - eventBus: null, - ), - ), + builder: (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Loading currencies", + eventBus: null, ), + ), + ), ), ); @@ -101,36 +102,33 @@ class _ExchangeCurrencySelectionViewState Future> _loadCurrencies() async { await ExchangeDataLoadingService.instance.initDB(); final isar = await ExchangeDataLoadingService.instance.isar; - final currencies = - await isar.currencies - .where() - .filter() - .isFiatEqualTo(false) - .and() - .group( - (q) => - widget.isFixedRate - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .sortByIsStackCoin() - .thenByName() - .findAll(); + final currencies = await isar.currencies + .where() + .filter() + .isFiatEqualTo(false) + .and() + .group( + (q) => widget.isFixedRate + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .sortByIsStackCoin() + .thenByName() + .findAll(); // If using Tor, filter exchanges which do not support Tor. if (Prefs.instance.useTor) { if (Exchange.exchangeNamesWithTorSupport.isNotEmpty) { currencies.removeWhere( - (element) => - !Exchange.exchangeNamesWithTorSupport.contains( - element.exchangeName, - ), + (element) => !Exchange.exchangeNamesWithTorSupport.contains( + element.exchangeName, + ), ); } } @@ -163,20 +161,18 @@ class _ExchangeCurrencySelectionViewState results.remove(widget.pairedCurrency); } - final walletCoins = - results - .where( - (currency) => - AppConfig.coins - .where( - (coin) => - coin.ticker.toLowerCase() == - currency.ticker.toLowerCase() && - currency.fuzzyNet == coin.ticker.toLowerCase(), - ) - .isNotEmpty, - ) - .toList(); + final walletCoins = results + .where( + (currency) => AppConfig.coins + .where( + (coin) => + coin.ticker.toLowerCase() == + currency.ticker.toLowerCase() && + currency.fuzzyNet == coin.ticker.toLowerCase(), + ) + .isNotEmpty, + ) + .toList(); final list = results.toList(); @@ -240,8 +236,9 @@ class _ExchangeCurrencySelectionViewState builder: (child) { return Background( child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () async { @@ -286,45 +283,45 @@ class _ExchangeCurrencySelectionViewState focusNode: _searchFocusNode, onChanged: (value) => setState(() => _searchString = value), style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - _searchFocusNode, - context, - desktopMed: isDesktop, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, - ), - ), - suffixIcon: - _searchController.text.isNotEmpty + decoration: + standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) + ) : null, - ), + ), ), ), const SizedBox(height: 20), @@ -358,23 +355,20 @@ class _ExchangeCurrencySelectionViewState height: 24, child: AppConfig.isStackCoin( - items[index].ticker, - ) - ? CoinIconForTicker( - ticker: items[index].ticker, - size: 24, - ) - : hasImageUrl - ? _NetImage( - url: image, - key: ValueKey( - image + items[index].fuzzyNet, - ), - ) - : const SizedBox( - width: 24, - height: 24, + items[index].ticker, + ) + ? CoinIconForTicker( + ticker: items[index].ticker, + size: 24, + ) + : hasImageUrl + ? _NetImage( + url: image, + key: ValueKey( + image + items[index].fuzzyNet, ), + ) + : const SizedBox(width: 24, height: 24), ), const SizedBox(width: 10), Expanded( @@ -399,9 +393,8 @@ class _ExchangeCurrencySelectionViewState left: 12, ), child: CoinTickerTag( - ticker: - items[index].fuzzyNet - .toUpperCase(), + ticker: items[index].fuzzyNet + .toUpperCase(), ), ), ], @@ -409,14 +402,12 @@ class _ExchangeCurrencySelectionViewState const SizedBox(height: 2), Text( items[index].ticker.toUpperCase(), - style: STextStyles.smallMed12( - context, - ).copyWith( - color: - Theme.of(context) + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) .extension()! .textSubtitle1, - ), + ), ), ], ), @@ -447,15 +438,20 @@ class _NetImage extends StatelessWidget { @override Widget build(BuildContext context) { if (url.endsWith(".svg")) { - return SvgPicture.network( + return SvgPicture( key: key, - url, + TorAwareSvgNetworkLoader(url), width: 24, height: 24, placeholderBuilder: (_) => const LoadingIndicator(), ); } else { - return Image.network(url, width: 24, height: 24, key: key); + return Image( + image: TorAwareNetworkImage(url), + width: 24, + height: 24, + key: key, + ); } } } diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart index 700088664c..95d2ee5265 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_option.dart @@ -15,6 +15,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app_config.dart'; import '../../../models/exchange/aggregate_currency.dart'; import '../../../models/exchange/response_objects/estimate.dart'; +import '../../../networking/tor_aware_network_image.dart'; import '../../../providers/exchange/exchange_form_state_provider.dart'; import '../../../providers/global/locale_provider.dart'; import '../../../services/exchange/exchange.dart'; @@ -164,9 +165,9 @@ class _ExchangeOptionState extends ConsumerState { isDesktop ? Container( height: 1, - color: Theme.of( - context, - ).extension()!.background, + color: Theme.of(context) + .extension()! + .background, ) : const SizedBox(height: 16), child, @@ -215,9 +216,9 @@ class _ExchangeOptionState extends ConsumerState { estimate: null, pair: pair, rateString: message ?? "Failed to fetch rate", - rateColor: Theme.of( - context, - ).extension()!.textError, + rateColor: Theme.of(context) + .extension()! + .textError, ); }, ); @@ -354,9 +355,9 @@ class _ProviderOptionState extends ConsumerState { child: Padding( padding: EdgeInsets.only(top: isDesktop ? 20.0 : 15.0), child: Radio( - activeColor: Theme.of( - context, - ).extension()!.radioButtonIconEnabled, + activeColor: Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: _id, groupValue: groupValue, onChanged: (_) { @@ -382,8 +383,10 @@ class _ProviderOptionState extends ConsumerState { widget.estimate!.exchangeProviderLogo!.isNotEmpty ? ClipRRect( borderRadius: BorderRadius.circular(5), - child: Image.network( - widget.estimate!.exchangeProviderLogo!, + child: Image( + image: TorAwareNetworkImage( + widget.estimate!.exchangeProviderLogo!, + ), loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) { @@ -427,9 +430,9 @@ class _ProviderOptionState extends ConsumerState { widget.estimate?.exchangeProvider ?? widget.exchange.name, style: STextStyles.titleBold12(context).copyWith( - color: Theme.of( - context, - ).extension()!.textDark2, + color: Theme.of(context) + .extension()! + .textDark2, ), ), ), @@ -443,9 +446,9 @@ class _ProviderOptionState extends ConsumerState { ], style: STextStyles.itemSubtitle12(context) .copyWith( - color: Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of(context) + .extension()! + .textSubtitle1, ), ) : Text( diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart index b350fa527b..93e0f71da4 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart @@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar_community/isar.dart'; import '../../../../../models/isar/stack_theme.dart'; +import '../../../../../networking/tor_aware_network_image.dart'; import '../../../../../notifications/show_flush_bar.dart'; import '../../../../../providers/db/main_db_provider.dart'; import '../../../../../providers/global/prefs_provider.dart'; @@ -69,18 +70,16 @@ class _StackThemeCardState extends ConsumerState { } Future _downloadPressed() async { - final result = - (await showLoading( - whileFuture: _downloadAndInstall(), - context: context, - message: "Downloading and installing theme...", - ))!; + final result = (await showLoading( + whileFuture: _downloadAndInstall(), + context: context, + message: "Downloading and installing theme...", + ))!; if (mounted) { - final message = - result - ? "${widget.data.name} theme installed!" - : "Failed to install ${widget.data.name} theme"; + final message = result + ? "${widget.data.name} theme installed!" + : "Failed to install ${widget.data.name} theme"; if (isDesktop) { await showFloatingFlushBar( type: result ? FlushBarType.success : FlushBarType.warning, @@ -90,16 +89,15 @@ class _StackThemeCardState extends ConsumerState { } else { await showDialog( context: context, - builder: - (_) => StackOkDialog( - title: message, - onOkPressed: (_) { - setState(() { - _needsUpdate = !result; - _hasTheme = result; - }); - }, - ), + builder: (_) => StackOkDialog( + title: message, + onOkPressed: (_) { + setState(() { + _needsUpdate = !result; + _hasTheme = result; + }); + }, + ), ); } } @@ -155,14 +153,13 @@ class _StackThemeCardState extends ConsumerState { } } - StackTheme? getInstalled() => - ref - .read(mainDBProvider) - .isar - .stackThemes - .where() - .themeIdEqualTo(widget.data.id) - .findFirstSync(); + StackTheme? getInstalled() => ref + .read(mainDBProvider) + .isar + .stackThemes + .where() + .themeIdEqualTo(widget.data.id) + .findFirstSync(); @override void initState() { @@ -207,39 +204,41 @@ class _StackThemeCardState extends ConsumerState { Widget build(BuildContext context) { return RoundedWhiteContainer( radiusMultiplier: isDesktop ? 2.5 : 1, - borderColor: - isDesktop - ? Theme.of(context).extension()!.textSubtitle6 - : null, + borderColor: isDesktop + ? Theme.of(context).extension()!.textSubtitle6 + : null, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 18), - child: - widget.data.previewImageUrl.isNotEmpty - ? AspectRatio( - aspectRatio: 1, - child: ClipRRect( - borderRadius: BorderRadius.circular(100), - child: Image.network(widget.data.previewImageUrl), + child: widget.data.previewImageUrl.isNotEmpty + ? AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(100), + child: Image( + image: TorAwareNetworkImage( + widget.data.previewImageUrl, + ), ), - ) - : Builder( - builder: (context) { - final themePreview = - ref - .watch(pThemeService) - .getTheme(themeId: widget.data.id) - ?.assets - .themePreview ?? - ""; - - return (themePreview.endsWith(".png")) - ? Image.file(File(themePreview), height: 100) - : SvgPicture.file(File(themePreview), height: 100); - }, ), + ) + : Builder( + builder: (context) { + final themePreview = + ref + .watch(pThemeService) + .getTheme(themeId: widget.data.id) + ?.assets + .themePreview ?? + ""; + + return (themePreview.endsWith(".png")) + ? Image.file(File(themePreview), height: 100) + : SvgPicture.file(File(themePreview), height: 100); + }, + ), ), const SizedBox(height: 12), Text(widget.data.name, style: STextStyles.itemSubtitle12(context)), @@ -276,10 +275,9 @@ class _StackThemeCardState extends ConsumerState { const SizedBox(height: 12), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: - _hasTheme - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: _hasTheme + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: PrimaryButton( label: "Download", buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l, diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart index 2a558ef5dc..5d99517f59 100644 --- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart +++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart @@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../db/drift/shared_db/shared_database.dart'; import '../../models/shopinbit/shopinbit_enums.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/db/drift_provider.dart'; import '../../providers/global/shopin_bit_service_provider.dart'; @@ -432,9 +433,8 @@ class _ShopInBitTicketDetailState extends ConsumerState ); } if (context.mounted) { - await Navigator.of( - context, - ).pushNamed(ShopInBitOfferView.routeName, arguments: _id); + await Navigator.of(context) + .pushNamed(ShopInBitOfferView.routeName, arguments: _id); } } @@ -443,9 +443,9 @@ class _ShopInBitTicketDetailState extends ConsumerState padding: .only(bottom: isDesktop ? 12 : 8), child: RoundedWhiteContainer( borderColor: isDesktop - ? Theme.of( - context, - ).extension()!.textFieldDefaultBG + ? Theme.of(context) + .extension()! + .textFieldDefaultBG : null, child: ConditionalParent( condition: Util.isDesktop, @@ -559,9 +559,9 @@ class _ShopInBitTicketDetailState extends ConsumerState ? STextStyles.desktopTextExtraSmall(context) : STextStyles.field(context)) .copyWith( - color: Theme.of( - context, - ).extension()!.textDark, + color: Theme.of(context) + .extension()! + .textDark, ), decoration: InputDecoration( hintText: "Type a message...", @@ -585,9 +585,9 @@ class _ShopInBitTicketDetailState extends ConsumerState Assets.svg.send, width: 24, height: 24, - color: Theme.of( - context, - ).extension()!.accentColorBlue, + color: Theme.of(context) + .extension()! + .accentColorBlue, ), ), ], @@ -600,9 +600,9 @@ class _ShopInBitTicketDetailState extends ConsumerState padding: EdgeInsets.only(bottom: isDesktop ? 12 : 8), child: RoundedWhiteContainer( borderColor: isDesktop - ? Theme.of( - context, - ).extension()!.textFieldDefaultBG + ? Theme.of(context) + .extension()! + .textFieldDefaultBG : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -698,9 +698,9 @@ class _ShopInBitTicketDetailState extends ConsumerState condition: !isDesktop, builder: (child) => Background( child: Scaffold( - backgroundColor: Theme.of( - context, - ).extension()!.background, + backgroundColor: Theme.of(context) + .extension()! + .background, appBar: AppBar( leading: AppBarBackButton( onPressed: () => Navigator.of(context).pop(), @@ -892,10 +892,13 @@ class _ProxyImageState extends State<_ProxyImage> { if (resp.hasError || resp.value == null) { return widget.fallback(); } - return Image.network( - resp.value!.toString(), + return Image( + image: ResizeImage.resizeIfNeeded( + null, + _kAttachmentDecodeHeight, + TorAwareNetworkImage(resp.value!.toString()), + ), fit: BoxFit.contain, - cacheHeight: _kAttachmentDecodeHeight, semanticLabel: "Image attachment", errorBuilder: (_, _, _) => widget.fallback(), ); diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart index eb88200916..4162ed6e23 100644 --- a/lib/widgets/icon_widgets/eth_token_icon.dart +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -16,6 +16,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar_community/isar.dart'; import '../../models/isar/exchange_cache/currency.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../themes/coin_icon_provider.dart'; @@ -87,8 +88,8 @@ class _EthTokenIconState extends ConsumerState { height: widget.size, ); } else { - return SvgPicture.network( - imageUrl!, + return SvgPicture( + TorAwareSvgNetworkLoader(imageUrl!), width: widget.size, height: widget.size, placeholderBuilder: (_) => const LoadingIndicator(), diff --git a/lib/widgets/icon_widgets/sol_token_icon.dart b/lib/widgets/icon_widgets/sol_token_icon.dart index 8907651c3b..24b8e71333 100644 --- a/lib/widgets/icon_widgets/sol_token_icon.dart +++ b/lib/widgets/icon_widgets/sol_token_icon.dart @@ -15,6 +15,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar_community/isar.dart'; import '../../models/isar/exchange_cache/currency.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../services/exchange/change_now/change_now_exchange.dart'; import '../../services/exchange/exchange_data_loading_service.dart'; import '../../themes/coin_icon_provider.dart'; @@ -84,8 +85,8 @@ class _SolTokenIconState extends ConsumerState { ); } else { // Display token icon from network. - return SvgPicture.network( - imageUrl!, + return SvgPicture( + TorAwareSvgNetworkLoader(imageUrl!), width: widget.size, height: widget.size, placeholderBuilder: (_) => const LoadingIndicator(), diff --git a/lib/widgets/textfields/exchange_textfield.dart b/lib/widgets/textfields/exchange_textfield.dart index 3229b28d5c..b9f3530386 100644 --- a/lib/widgets/textfields/exchange_textfield.dart +++ b/lib/widgets/textfields/exchange_textfield.dart @@ -14,6 +14,7 @@ import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; import '../../models/exchange/aggregate_currency.dart'; +import '../../networking/tor_aware_network_image.dart'; import '../../pages/buy_view/sub_widgets/crypto_selection_view.dart'; import '../../providers/global/locale_provider.dart'; import '../../themes/stack_colors.dart'; @@ -181,8 +182,10 @@ class _ExchangeTextFieldState extends ConsumerState { } else if (widget.currency != null && widget.currency!.image.isNotEmpty) { return Center( - child: SvgPicture.network( - widget.currency!.image, + child: SvgPicture( + TorAwareSvgNetworkLoader( + widget.currency!.image, + ), height: 18, placeholderBuilder: (_) => Container( width: 18, diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index e5450c39ad..e341a23ea1 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -16,6 +16,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar_community/isar.dart'; import '../../../models/isar/exchange_cache/currency.dart'; +import '../../../networking/tor_aware_network_image.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; import '../../../themes/coin_icon_provider.dart'; @@ -48,18 +49,14 @@ class _WalletInfoCoinIconState extends ConsumerState { ExchangeDataLoadingService.instance.isar.then((isar) async { if (widget.contractAddress != null) { - final currency = - await isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - widget.contractAddress!, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirst(); + final currency = await isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo(widget.contractAddress!, caseSensitive: false) + .and() + .imageIsNotEmpty() + .findFirst(); if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -87,14 +84,17 @@ class _WalletInfoCoinIconState extends ConsumerState { ), child: Padding( padding: EdgeInsets.all(widget.size / 5), - child: - imageUrl != null && imageUrl!.isNotEmpty - ? SvgPicture.network(imageUrl!, width: 20, height: 20) - : SvgPicture.file( - File(ref.watch(coinIconProvider(widget.coin))), - width: 20, - height: 20, - ), + child: imageUrl != null && imageUrl!.isNotEmpty + ? SvgPicture( + TorAwareSvgNetworkLoader(imageUrl!), + width: 20, + height: 20, + ) + : SvgPicture.file( + File(ref.watch(coinIconProvider(widget.coin))), + width: 20, + height: 20, + ), ), ); }