diff --git a/.gitignore b/.gitignore
index ed66fa960..e58a9412f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,4 +55,10 @@ libcw_monero.dll
libcw_wownero.dll
libepic_cash_wallet.dll
libmobileliblelantus.dll
+libtor_ffi.dll
/libisar.so
+libtor_ffi.so
+
+tor_logs.txt
+
+torrc
diff --git a/.gitmodules b/.gitmodules
index 66ee203f4..3aa333e4a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -7,6 +7,9 @@
[submodule "crypto_plugins/flutter_liblelantus"]
path = crypto_plugins/flutter_liblelantus
url = https://github.com/cypherstack/flutter_liblelantus.git
+[submodule "crypto_plugins/tor"]
+ path = crypto_plugins/tor
+ url = https://github.com/cypherstack/tor.git
[submodule "fusiondart"]
path = fusiondart
- url = https://github.com/cypherstack/fusiondart
+ url = https://github.com/cypherstack/fusiondart.git
diff --git a/assets/default_themes/dark.zip b/assets/default_themes/dark.zip
index fe11a5463..eb20d7e6e 100644
Binary files a/assets/default_themes/dark.zip and b/assets/default_themes/dark.zip differ
diff --git a/assets/default_themes/light.zip b/assets/default_themes/light.zip
index 1453d6ba0..0048f661c 100644
Binary files a/assets/default_themes/light.zip and b/assets/default_themes/light.zip differ
diff --git a/assets/svg/connected-button.svg b/assets/svg/connected-button.svg
new file mode 100644
index 000000000..96a9970c0
--- /dev/null
+++ b/assets/svg/connected-button.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svg/connecting-button.svg b/assets/svg/connecting-button.svg
new file mode 100644
index 000000000..1bc6e953b
--- /dev/null
+++ b/assets/svg/connecting-button.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svg/disconnected-button.svg b/assets/svg/disconnected-button.svg
new file mode 100644
index 000000000..03a8067d7
--- /dev/null
+++ b/assets/svg/disconnected-button.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svg/tor-circle.svg b/assets/svg/tor-circle.svg
new file mode 100644
index 000000000..8268a00f6
--- /dev/null
+++ b/assets/svg/tor-circle.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/svg/tor-synced.svg b/assets/svg/tor-synced.svg
new file mode 100644
index 000000000..20cff1f37
--- /dev/null
+++ b/assets/svg/tor-synced.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svg/tor-syncing.svg b/assets/svg/tor-syncing.svg
new file mode 100644
index 000000000..b51803c70
--- /dev/null
+++ b/assets/svg/tor-syncing.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/svg/tor.svg b/assets/svg/tor.svg
new file mode 100644
index 000000000..a893c0907
--- /dev/null
+++ b/assets/svg/tor.svg
@@ -0,0 +1,4 @@
+
diff --git a/crypto_plugins/tor b/crypto_plugins/tor
new file mode 160000
index 000000000..a819223b2
--- /dev/null
+++ b/crypto_plugins/tor
@@ -0,0 +1 @@
+Subproject commit a819223b23e9fa1d76bde82ed9109651e96f2ad3
diff --git a/docs/building.md b/docs/building.md
index b56a87899..e7128df2e 100644
--- a/docs/building.md
+++ b/docs/building.md
@@ -123,7 +123,7 @@ flutter run android
Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work
#### Linux
-Plug in your android device or use the emulator available via Android Studio and then run the following commands:
+Run the following commands or launch via Android Studio:
```
flutter pub get
flutter run linux
diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart
index cb5237bab..8366e259f 100644
--- a/lib/electrumx_rpc/cached_electrumx.dart
+++ b/lib/electrumx_rpc/cached_electrumx.dart
@@ -9,7 +9,6 @@
*/
import 'dart:convert';
-import 'dart:math';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
@@ -158,6 +157,7 @@ class CachedElectrumX {
Future> getUsedCoinSerials({
required Coin coin,
+ int startNumber = 0,
}) async {
try {
final box = await DB.instance.getUsedSerialsCacheBox(coin: coin);
@@ -168,7 +168,7 @@ class CachedElectrumX {
_list == null ? {} : List.from(_list).toSet();
final startNumber =
- max(0, cachedSerials.length - 100); // 100 being some arbitrary buffer
+ cachedSerials.length - 10; // 10 being some arbitrary buffer
final serials = await electrumXClient.getUsedCoinSerials(
startNumber: startNumber,
diff --git a/lib/electrumx_rpc/electrumx.dart b/lib/electrumx_rpc/electrumx.dart
index c9417116d..292679342 100644
--- a/lib/electrumx_rpc/electrumx.dart
+++ b/lib/electrumx_rpc/electrumx.dart
@@ -8,13 +8,20 @@
*
*/
+import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart';
+import 'package:event_bus/event_bus.dart';
+import 'package:mutex/mutex.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/global_event_bus.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart';
@@ -65,12 +72,27 @@ class ElectrumX {
JsonRPC? _rpcClient;
late Prefs _prefs;
+ late TorService _torService;
List? failovers;
int currentFailoverIndex = -1;
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
+ // add finalizer to cancel stream subscription when all references to an
+ // instance of ElectrumX becomes inaccessible
+ static final Finalizer _finalizer = Finalizer(
+ (p0) {
+ p0._torPreferenceListener?.cancel();
+ p0._torStatusListener?.cancel();
+ },
+ );
+ StreamSubscription? _torPreferenceListener;
+ StreamSubscription? _torStatusListener;
+
+ final Mutex _torConnectingLock = Mutex();
+ bool _requireMutex = false;
+
ElectrumX({
required String host,
required int port,
@@ -80,26 +102,79 @@ class ElectrumX {
JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60),
+ TorService? torService,
+ EventBus? globalEventBusForTesting,
}) {
_prefs = prefs;
+ _torService = torService ?? TorService.sharedInstance;
_host = host;
_port = port;
_useSSL = useSSL;
_rpcClient = client;
+
+ final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
+ _torStatusListener = bus.on().listen(
+ (event) async {
+ switch (event.newStatus) {
+ case TorConnectionStatus.connecting:
+ await _torConnectingLock.acquire();
+ _requireMutex = true;
+ break;
+
+ case TorConnectionStatus.connected:
+ case TorConnectionStatus.disconnected:
+ if (_torConnectingLock.isLocked) {
+ _torConnectingLock.release();
+ }
+ _requireMutex = false;
+ break;
+ }
+ },
+ );
+ _torPreferenceListener = bus.on().listen(
+ (event) async {
+ // not sure if we need to do anything specific here
+ // switch (event.status) {
+ // case TorStatus.enabled:
+ // case TorStatus.disabled:
+ // }
+
+ // might be ok to just reset/kill the current _jsonRpcClient
+
+ // since disconnecting is async and we want to ensure instant change over
+ // we will keep temp reference to current rpc client to call disconnect
+ // on before awaiting the disconnection future
+
+ final temp = _rpcClient;
+
+ // setting to null should force the creation of a new json rpc client
+ // on the next request sent through this electrumx instance
+ _rpcClient = null;
+
+ await temp?.disconnect(
+ reason: "Tor status changed to \"${event.status}\"",
+ );
+ },
+ );
}
factory ElectrumX.from({
required ElectrumXNode node,
required Prefs prefs,
required List failovers,
- }) =>
- ElectrumX(
- host: node.address,
- port: node.port,
- useSSL: node.useSSL,
- prefs: prefs,
- failovers: failovers,
- );
+ TorService? torService,
+ EventBus? globalEventBusForTesting,
+ }) {
+ return ElectrumX(
+ host: node.address,
+ port: node.port,
+ useSSL: node.useSSL,
+ prefs: prefs,
+ torService: torService,
+ failovers: failovers,
+ globalEventBusForTesting: globalEventBusForTesting,
+ );
+ }
Future _allow() async {
if (_prefs.wifiOnly) {
@@ -109,6 +184,75 @@ class ElectrumX {
return true;
}
+ void _checkRpcClient() {
+ // If we're supposed to use Tor...
+ if (_prefs.useTor) {
+ // But Tor isn't enabled...
+ if (!_torService.enabled) {
+ // And the killswitch isn't set...
+ if (!_prefs.torKillSwitch) {
+ // Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
+ Logging.instance.log(
+ "Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet",
+ level: LogLevel.Warning,
+ );
+ } else {
+ // ... But if the killswitch is set, then we throw an exception.
+ throw Exception(
+ "Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX");
+ }
+ } else {
+ // Get the proxy info from the TorService.
+ final proxyInfo = _torService.proxyInfo;
+
+ if (currentFailoverIndex == -1) {
+ _rpcClient ??= JsonRPC(
+ host: host,
+ port: port,
+ useSSL: useSSL,
+ connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
+ proxyInfo: proxyInfo,
+ );
+ } else {
+ _rpcClient ??= JsonRPC(
+ host: failovers![currentFailoverIndex].address,
+ port: failovers![currentFailoverIndex].port,
+ useSSL: failovers![currentFailoverIndex].useSSL,
+ connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
+ proxyInfo: proxyInfo,
+ );
+ }
+
+ if (_rpcClient!.proxyInfo != proxyInfo) {
+ _rpcClient!.proxyInfo = proxyInfo;
+ _rpcClient!.disconnect(
+ reason: "Tor proxyInfo does not match current info",
+ );
+ }
+
+ return;
+ }
+ }
+
+ if (currentFailoverIndex == -1) {
+ _rpcClient ??= JsonRPC(
+ host: host,
+ port: port,
+ useSSL: useSSL,
+ connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
+ proxyInfo: null,
+ );
+ } else {
+ _rpcClient ??= JsonRPC(
+ host: failovers![currentFailoverIndex].address,
+ port: failovers![currentFailoverIndex].port,
+ useSSL: failovers![currentFailoverIndex].useSSL,
+ connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
+ proxyInfo: null,
+ );
+ }
+ }
+
/// Send raw rpc command
Future request({
required String command,
@@ -121,20 +265,10 @@ class ElectrumX {
throw WifiOnlyException();
}
- if (currentFailoverIndex == -1) {
- _rpcClient ??= JsonRPC(
- host: host,
- port: port,
- useSSL: useSSL,
- connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
- );
+ if (_requireMutex) {
+ await _torConnectingLock.protect(() async => _checkRpcClient());
} else {
- _rpcClient = JsonRPC(
- host: failovers![currentFailoverIndex].address,
- port: failovers![currentFailoverIndex].port,
- useSSL: failovers![currentFailoverIndex].useSSL,
- connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
- );
+ _checkRpcClient();
}
try {
@@ -221,20 +355,10 @@ class ElectrumX {
throw WifiOnlyException();
}
- if (currentFailoverIndex == -1) {
- _rpcClient ??= JsonRPC(
- host: host,
- port: port,
- useSSL: useSSL,
- connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
- );
+ if (_requireMutex) {
+ await _torConnectingLock.protect(() async => _checkRpcClient());
} else {
- _rpcClient = JsonRPC(
- host: failovers![currentFailoverIndex].address,
- port: failovers![currentFailoverIndex].port,
- useSSL: failovers![currentFailoverIndex].useSSL,
- connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
- );
+ _checkRpcClient();
}
try {
diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart
index 2dc0d3a71..3d2d499a6 100644
--- a/lib/electrumx_rpc/rpc.dart
+++ b/lib/electrumx_rpc/rpc.dart
@@ -14,7 +14,10 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.dart';
+import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart';
+import 'package:stackwallet/networking/socks_socket.dart';
import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/prefs.dart';
// Json RPC class to handle connecting to electrumx servers
class JsonRPC {
@@ -23,16 +26,19 @@ class JsonRPC {
required this.port,
this.useSSL = false,
this.connectionTimeout = const Duration(seconds: 60),
+ required ({InternetAddress host, int port})? proxyInfo,
});
final bool useSSL;
final String host;
final int port;
final Duration connectionTimeout;
+ ({InternetAddress host, int port})? proxyInfo;
final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket;
- StreamSubscription? _subscription;
+ SOCKSSocket? _socksSocket;
+ StreamSubscription>? _subscription;
void _dataHandler(List data) {
_requestQueue.nextIncompleteReq.then((req) {
@@ -75,7 +81,12 @@ class JsonRPC {
_requestQueue.nextIncompleteReq.then((req) {
if (req != null) {
// \r\n required by electrumx server
- _socket!.write('${req.jsonRequest}\r\n');
+ if (_socket != null) {
+ _socket!.write('${req.jsonRequest}\r\n');
+ }
+ if (_socksSocket != null) {
+ _socksSocket!.write('${req.jsonRequest}\r\n');
+ }
// TODO different timeout length?
req.initiateTimeout(
@@ -92,12 +103,22 @@ class JsonRPC {
Duration requestTimeout,
) async {
await _requestMutex.protect(() async {
- if (_socket == null) {
- Logging.instance.log(
- "JsonRPC request: opening socket $host:$port",
- level: LogLevel.Info,
- );
- await connect();
+ if (!Prefs.instance.useTor) {
+ if (_socket == null) {
+ Logging.instance.log(
+ "JsonRPC request: opening socket $host:$port",
+ level: LogLevel.Info,
+ );
+ await connect();
+ }
+ } else {
+ if (_socksSocket == null) {
+ Logging.instance.log(
+ "JsonRPC request: opening SOCKS socket to $host:$port",
+ level: LogLevel.Info,
+ );
+ await connect();
+ }
}
});
@@ -113,9 +134,9 @@ class JsonRPC {
reason: "return req.completer.future.onError: $error\n$stackTrace",
);
return JsonRPCResponse(
- exception: error is Exception
+ exception: error is JsonRpcException
? error
- : Exception(
+ : JsonRpcException(
"req.completer.future.onError: $error\n$stackTrace",
),
);
@@ -137,6 +158,8 @@ class JsonRPC {
_subscription = null;
_socket?.destroy();
_socket = null;
+ await _socksSocket?.close();
+ _socksSocket = null;
// clean up remaining queue
await _requestQueue.completeRemainingWithError(
@@ -146,33 +169,84 @@ class JsonRPC {
}
Future connect() async {
- if (_socket != null) {
- throw Exception(
- "JsonRPC attempted to connect to an already existing socket!",
- );
- }
+ if (!Prefs.instance.useTor) {
+ if (useSSL) {
+ _socket = await SecureSocket.connect(
+ host,
+ port,
+ timeout: connectionTimeout,
+ onBadCertificate: (_) => true,
+ ); // TODO do not automatically trust bad certificates
+ } else {
+ _socket = await Socket.connect(
+ host,
+ port,
+ timeout: connectionTimeout,
+ );
+ }
- if (useSSL) {
- _socket = await SecureSocket.connect(
- host,
- port,
- timeout: connectionTimeout,
- onBadCertificate: (_) => true,
- ); // TODO do not automatically trust bad certificates
+ _subscription = _socket!.listen(
+ _dataHandler,
+ onError: _errorHandler,
+ onDone: _doneHandler,
+ cancelOnError: true,
+ );
} else {
- _socket = await Socket.connect(
- host,
- port,
- timeout: connectionTimeout,
+ if (proxyInfo == null) {
+ throw JsonRpcException(
+ "JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null");
+ }
+
+ // instantiate a socks socket at localhost and on the port selected by the tor service
+ _socksSocket = await SOCKSSocket.create(
+ proxyHost: proxyInfo!.host.address,
+ proxyPort: proxyInfo!.port,
+ sslEnabled: useSSL,
+ );
+
+ try {
+ Logging.instance.log(
+ "JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
+ level: LogLevel.Info);
+
+ await _socksSocket?.connect();
+
+ Logging.instance.log(
+ "JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...",
+ level: LogLevel.Info);
+ } catch (e) {
+ Logging.instance.log(
+ "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e",
+ level: LogLevel.Error);
+ throw JsonRpcException(
+ "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e");
+ }
+
+ try {
+ Logging.instance.log(
+ "JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
+ level: LogLevel.Info);
+
+ await _socksSocket?.connectTo(host, port);
+
+ Logging.instance.log(
+ "JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
+ level: LogLevel.Info);
+ } catch (e) {
+ Logging.instance.log(
+ "JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
+ level: LogLevel.Error);
+ throw JsonRpcException(
+ "JsonRPC.connect(): failed to connect to tor proxy, $e");
+ }
+
+ _subscription = _socksSocket!.listen(
+ _dataHandler,
+ onError: _errorHandler,
+ onDone: _doneHandler,
+ cancelOnError: true,
);
}
-
- _subscription = _socket!.listen(
- _dataHandler,
- onError: _errorHandler,
- onDone: _doneHandler,
- cancelOnError: true,
- );
}
}
@@ -277,7 +351,7 @@ class _JsonRPCRequest {
Future.delayed(requestTimeout).then((_) {
if (!isComplete) {
try {
- throw Exception("_JsonRPCRequest timed out: $jsonRequest");
+ throw JsonRpcException("_JsonRPCRequest timed out: $jsonRequest");
} catch (e, s) {
completer.completeError(e, s);
onTimedOut?.call();
@@ -291,7 +365,18 @@ class _JsonRPCRequest {
class JsonRPCResponse {
final dynamic data;
- final Exception? exception;
+ final JsonRpcException? exception;
JsonRPCResponse({this.data, this.exception});
}
+
+bool isIpAddress(String host) {
+ try {
+ // if the string can be parsed into an InternetAddress, it's an IP.
+ InternetAddress(host);
+ return true;
+ } catch (e) {
+ // if parsing fails, it's not an IP.
+ return false;
+ }
+}
diff --git a/lib/electrumx_rpc/subscribable_electrumx.dart b/lib/electrumx_rpc/subscribable_electrumx.dart
index 4720e345b..b7da56a52 100644
--- a/lib/electrumx_rpc/subscribable_electrumx.dart
+++ b/lib/electrumx_rpc/subscribable_electrumx.dart
@@ -1,324 +1,324 @@
-/*
- * This file is part of Stack Wallet.
- *
- * Copyright (c) 2023 Cypher Stack
- * All Rights Reserved.
- * The code is distributed under GPLv3 license, see LICENSE file for details.
- * Generated by Cypher Stack on 2023-05-26
- *
- */
-
-import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
-
-import 'package:flutter/foundation.dart';
-import 'package:stackwallet/utilities/logger.dart';
-
-class ElectrumXSubscription with ChangeNotifier {
- dynamic _response;
- dynamic get response => _response;
- set response(dynamic newData) {
- _response = newData;
- notifyListeners();
- }
-}
-
-class SocketTask {
- SocketTask({this.completer, this.subscription});
-
- final Completer? completer;
- final ElectrumXSubscription? subscription;
-
- bool get isSubscription => subscription != null;
-}
-
-class SubscribableElectrumXClient {
- int _currentRequestID = 0;
- bool _isConnected = false;
- List _responseData = [];
- final Map _tasks = {};
- Timer? _aliveTimer;
- Socket? _socket;
- late final bool _useSSL;
- late final Duration _connectionTimeout;
- late final Duration _keepAlive;
-
- bool get isConnected => _isConnected;
- bool get useSSL => _useSSL;
-
- void Function(bool)? onConnectionStatusChanged;
-
- SubscribableElectrumXClient({
- bool useSSL = true,
- this.onConnectionStatusChanged,
- Duration connectionTimeout = const Duration(seconds: 5),
- Duration keepAlive = const Duration(seconds: 10),
- }) {
- _useSSL = useSSL;
- _connectionTimeout = connectionTimeout;
- _keepAlive = keepAlive;
- }
-
- Future connect({required String host, required int port}) async {
- try {
- await _socket?.close();
- } catch (_) {}
-
- if (_useSSL) {
- _socket = await SecureSocket.connect(
- host,
- port,
- timeout: _connectionTimeout,
- onBadCertificate: (_) => true,
- );
- } else {
- _socket = await Socket.connect(
- host,
- port,
- timeout: _connectionTimeout,
- );
- }
- _updateConnectionStatus(true);
-
- _socket!.listen(
- _dataHandler,
- onError: _errorHandler,
- onDone: _doneHandler,
- cancelOnError: true,
- );
-
- _aliveTimer?.cancel();
- _aliveTimer = Timer.periodic(
- _keepAlive,
- (_) async => _updateConnectionStatus(await ping()),
- );
- }
-
- Future disconnect() async {
- _aliveTimer?.cancel();
- await _socket?.close();
- onConnectionStatusChanged = null;
- }
-
- String _buildJsonRequestString({
- required String method,
- required String id,
- required List params,
- }) {
- final paramString = jsonEncode(params);
- return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
- }
-
- void _updateConnectionStatus(bool connectionStatus) {
- if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
- onConnectionStatusChanged!(connectionStatus);
- }
- _isConnected = connectionStatus;
- }
-
- void _dataHandler(List data) {
- _responseData.addAll(data);
-
- // 0x0A is newline
- // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
- if (data.last == 0x0A) {
- try {
- final response = jsonDecode(String.fromCharCodes(_responseData))
- as Map;
- _responseHandler(response);
- } catch (e, s) {
- Logging.instance
- .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
- rethrow;
- } finally {
- _responseData = [];
- }
- }
- }
-
- void _responseHandler(Map response) {
- // subscriptions will have a method in the response
- if (response['method'] is String) {
- _subscriptionHandler(response: response);
- return;
- }
-
- final id = response['id'] as String;
- final result = response['result'];
-
- _complete(id, result);
- }
-
- void _subscriptionHandler({
- required Map response,
- }) {
- final method = response['method'];
- switch (method) {
- case "blockchain.scripthash.subscribe":
- final params = response["params"] as List;
- final scripthash = params.first as String;
- final taskId = "blockchain.scripthash.subscribe:$scripthash";
-
- _tasks[taskId]?.subscription?.response = params.last;
- break;
- case "blockchain.headers.subscribe":
- final params = response["params"];
- const taskId = "blockchain.headers.subscribe";
-
- _tasks[taskId]?.subscription?.response = params.first;
- break;
- default:
- break;
- }
- }
-
- void _errorHandler(Object error, StackTrace trace) {
- _updateConnectionStatus(false);
- Logging.instance.log(
- "SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
- level: LogLevel.Info);
- }
-
- void _doneHandler() {
- _updateConnectionStatus(false);
- Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
- level: LogLevel.Info);
- }
-
- void _complete(String id, dynamic data) {
- if (_tasks[id] == null) {
- return;
- }
-
- if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
- _tasks[id]?.completer?.complete(data);
- }
-
- if (!(_tasks[id]?.isSubscription ?? false)) {
- _tasks.remove(id);
- } else {
- _tasks[id]?.subscription?.response = data;
- }
- }
-
- void _addTask({
- required String id,
- required Completer completer,
- }) {
- _tasks[id] = SocketTask(completer: completer, subscription: null);
- }
-
- void _addSubscriptionTask({
- required String id,
- required ElectrumXSubscription subscription,
- }) {
- _tasks[id] = SocketTask(completer: null, subscription: subscription);
- }
-
- Future _call({
- required String method,
- List params = const [],
- }) async {
- final completer = Completer();
- _currentRequestID++;
- final id = _currentRequestID.toString();
- _addTask(id: id, completer: completer);
-
- _socket?.write(
- _buildJsonRequestString(
- method: method,
- id: id,
- params: params,
- ),
- );
-
- return completer.future;
- }
-
- Future _callWithTimeout({
- required String method,
- List params = const [],
- Duration timeout = const Duration(seconds: 2),
- }) async {
- final completer = Completer();
- _currentRequestID++;
- final id = _currentRequestID.toString();
- _addTask(id: id, completer: completer);
-
- _socket?.write(
- _buildJsonRequestString(
- method: method,
- id: id,
- params: params,
- ),
- );
-
- Timer(timeout, () {
- if (!completer.isCompleted) {
- completer.completeError(
- Exception("Request \"id: $id, method: $method\" timed out!"),
- );
- }
- });
-
- return completer.future;
- }
-
- ElectrumXSubscription _subscribe({
- required String taskId,
- required String method,
- List params = const [],
- }) {
- // try {
- final subscription = ElectrumXSubscription();
- _addSubscriptionTask(id: taskId, subscription: subscription);
- _currentRequestID++;
- _socket?.write(
- _buildJsonRequestString(
- method: method,
- id: taskId,
- params: params,
- ),
- );
-
- return subscription;
- // } catch (e, s) {
- // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
- // return null;
- // }
- }
-
- /// Ping the server to ensure it is responding
- ///
- /// Returns true if ping succeeded
- Future ping() async {
- try {
- final response = (await _callWithTimeout(method: "server.ping")) as Map;
- return response.keys.contains("result") && response["result"] == null;
- } catch (_) {
- return false;
- }
- }
-
- /// Subscribe to a scripthash to receive notifications on status changes
- ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
- return _subscribe(
- taskId: 'blockchain.scripthash.subscribe:$scripthash',
- method: 'blockchain.scripthash.subscribe',
- params: [scripthash],
- );
- }
-
- /// Subscribe to block headers to receive notifications on new blocks found
- ///
- /// Returns the existing subscription if found
- ElectrumXSubscription subscribeToBlockHeaders() {
- return _tasks["blockchain.headers.subscribe"]?.subscription ??
- _subscribe(
- taskId: "blockchain.headers.subscribe",
- method: "blockchain.headers.subscribe",
- params: [],
- );
- }
-}
+// /*
+// * This file is part of Stack Wallet.
+// *
+// * Copyright (c) 2023 Cypher Stack
+// * All Rights Reserved.
+// * The code is distributed under GPLv3 license, see LICENSE file for details.
+// * Generated by Cypher Stack on 2023-05-26
+// *
+// */
+//
+// import 'dart:async';
+// import 'dart:convert';
+// import 'dart:io';
+//
+// import 'package:flutter/foundation.dart';
+// import 'package:stackwallet/utilities/logger.dart';
+//
+// class ElectrumXSubscription with ChangeNotifier {
+// dynamic _response;
+// dynamic get response => _response;
+// set response(dynamic newData) {
+// _response = newData;
+// notifyListeners();
+// }
+// }
+//
+// class SocketTask {
+// SocketTask({this.completer, this.subscription});
+//
+// final Completer? completer;
+// final ElectrumXSubscription? subscription;
+//
+// bool get isSubscription => subscription != null;
+// }
+//
+// class SubscribableElectrumXClient {
+// int _currentRequestID = 0;
+// bool _isConnected = false;
+// List _responseData = [];
+// final Map _tasks = {};
+// Timer? _aliveTimer;
+// Socket? _socket;
+// late final bool _useSSL;
+// late final Duration _connectionTimeout;
+// late final Duration _keepAlive;
+//
+// bool get isConnected => _isConnected;
+// bool get useSSL => _useSSL;
+//
+// void Function(bool)? onConnectionStatusChanged;
+//
+// SubscribableElectrumXClient({
+// bool useSSL = true,
+// this.onConnectionStatusChanged,
+// Duration connectionTimeout = const Duration(seconds: 5),
+// Duration keepAlive = const Duration(seconds: 10),
+// }) {
+// _useSSL = useSSL;
+// _connectionTimeout = connectionTimeout;
+// _keepAlive = keepAlive;
+// }
+//
+// Future connect({required String host, required int port}) async {
+// try {
+// await _socket?.close();
+// } catch (_) {}
+//
+// if (_useSSL) {
+// _socket = await SecureSocket.connect(
+// host,
+// port,
+// timeout: _connectionTimeout,
+// onBadCertificate: (_) => true,
+// );
+// } else {
+// _socket = await Socket.connect(
+// host,
+// port,
+// timeout: _connectionTimeout,
+// );
+// }
+// _updateConnectionStatus(true);
+//
+// _socket!.listen(
+// _dataHandler,
+// onError: _errorHandler,
+// onDone: _doneHandler,
+// cancelOnError: true,
+// );
+//
+// _aliveTimer?.cancel();
+// _aliveTimer = Timer.periodic(
+// _keepAlive,
+// (_) async => _updateConnectionStatus(await ping()),
+// );
+// }
+//
+// Future disconnect() async {
+// _aliveTimer?.cancel();
+// await _socket?.close();
+// onConnectionStatusChanged = null;
+// }
+//
+// String _buildJsonRequestString({
+// required String method,
+// required String id,
+// required List params,
+// }) {
+// final paramString = jsonEncode(params);
+// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
+// }
+//
+// void _updateConnectionStatus(bool connectionStatus) {
+// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
+// onConnectionStatusChanged!(connectionStatus);
+// }
+// _isConnected = connectionStatus;
+// }
+//
+// void _dataHandler(List data) {
+// _responseData.addAll(data);
+//
+// // 0x0A is newline
+// // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
+// if (data.last == 0x0A) {
+// try {
+// final response = jsonDecode(String.fromCharCodes(_responseData))
+// as Map;
+// _responseHandler(response);
+// } catch (e, s) {
+// Logging.instance
+// .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
+// rethrow;
+// } finally {
+// _responseData = [];
+// }
+// }
+// }
+//
+// void _responseHandler(Map response) {
+// // subscriptions will have a method in the response
+// if (response['method'] is String) {
+// _subscriptionHandler(response: response);
+// return;
+// }
+//
+// final id = response['id'] as String;
+// final result = response['result'];
+//
+// _complete(id, result);
+// }
+//
+// void _subscriptionHandler({
+// required Map response,
+// }) {
+// final method = response['method'];
+// switch (method) {
+// case "blockchain.scripthash.subscribe":
+// final params = response["params"] as List;
+// final scripthash = params.first as String;
+// final taskId = "blockchain.scripthash.subscribe:$scripthash";
+//
+// _tasks[taskId]?.subscription?.response = params.last;
+// break;
+// case "blockchain.headers.subscribe":
+// final params = response["params"];
+// const taskId = "blockchain.headers.subscribe";
+//
+// _tasks[taskId]?.subscription?.response = params.first;
+// break;
+// default:
+// break;
+// }
+// }
+//
+// void _errorHandler(Object error, StackTrace trace) {
+// _updateConnectionStatus(false);
+// Logging.instance.log(
+// "SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
+// level: LogLevel.Info);
+// }
+//
+// void _doneHandler() {
+// _updateConnectionStatus(false);
+// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
+// level: LogLevel.Info);
+// }
+//
+// void _complete(String id, dynamic data) {
+// if (_tasks[id] == null) {
+// return;
+// }
+//
+// if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
+// _tasks[id]?.completer?.complete(data);
+// }
+//
+// if (!(_tasks[id]?.isSubscription ?? false)) {
+// _tasks.remove(id);
+// } else {
+// _tasks[id]?.subscription?.response = data;
+// }
+// }
+//
+// void _addTask({
+// required String id,
+// required Completer completer,
+// }) {
+// _tasks[id] = SocketTask(completer: completer, subscription: null);
+// }
+//
+// void _addSubscriptionTask({
+// required String id,
+// required ElectrumXSubscription subscription,
+// }) {
+// _tasks[id] = SocketTask(completer: null, subscription: subscription);
+// }
+//
+// Future _call({
+// required String method,
+// List params = const [],
+// }) async {
+// final completer = Completer();
+// _currentRequestID++;
+// final id = _currentRequestID.toString();
+// _addTask(id: id, completer: completer);
+//
+// _socket?.write(
+// _buildJsonRequestString(
+// method: method,
+// id: id,
+// params: params,
+// ),
+// );
+//
+// return completer.future;
+// }
+//
+// Future _callWithTimeout({
+// required String method,
+// List params = const [],
+// Duration timeout = const Duration(seconds: 2),
+// }) async {
+// final completer = Completer();
+// _currentRequestID++;
+// final id = _currentRequestID.toString();
+// _addTask(id: id, completer: completer);
+//
+// _socket?.write(
+// _buildJsonRequestString(
+// method: method,
+// id: id,
+// params: params,
+// ),
+// );
+//
+// Timer(timeout, () {
+// if (!completer.isCompleted) {
+// completer.completeError(
+// Exception("Request \"id: $id, method: $method\" timed out!"),
+// );
+// }
+// });
+//
+// return completer.future;
+// }
+//
+// ElectrumXSubscription _subscribe({
+// required String taskId,
+// required String method,
+// List params = const [],
+// }) {
+// // try {
+// final subscription = ElectrumXSubscription();
+// _addSubscriptionTask(id: taskId, subscription: subscription);
+// _currentRequestID++;
+// _socket?.write(
+// _buildJsonRequestString(
+// method: method,
+// id: taskId,
+// params: params,
+// ),
+// );
+//
+// return subscription;
+// // } catch (e, s) {
+// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
+// // return null;
+// // }
+// }
+//
+// /// Ping the server to ensure it is responding
+// ///
+// /// Returns true if ping succeeded
+// Future ping() async {
+// try {
+// final response = (await _callWithTimeout(method: "server.ping")) as Map;
+// return response.keys.contains("result") && response["result"] == null;
+// } catch (_) {
+// return false;
+// }
+// }
+//
+// /// Subscribe to a scripthash to receive notifications on status changes
+// ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
+// return _subscribe(
+// taskId: 'blockchain.scripthash.subscribe:$scripthash',
+// method: 'blockchain.scripthash.subscribe',
+// params: [scripthash],
+// );
+// }
+//
+// /// Subscribe to block headers to receive notifications on new blocks found
+// ///
+// /// Returns the existing subscription if found
+// ElectrumXSubscription subscribeToBlockHeaders() {
+// return _tasks["blockchain.headers.subscribe"]?.subscription ??
+// _subscribe(
+// taskId: "blockchain.headers.subscribe",
+// method: "blockchain.headers.subscribe",
+// params: [],
+// );
+// }
+// }
diff --git a/lib/exceptions/json_rpc/json_rpc_exception.dart b/lib/exceptions/json_rpc/json_rpc_exception.dart
new file mode 100644
index 000000000..e0a51ce84
--- /dev/null
+++ b/lib/exceptions/json_rpc/json_rpc_exception.dart
@@ -0,0 +1,21 @@
+/*
+ * This file is part of Stack Wallet.
+ *
+ * Copyright (c) 2023 Cypher Stack
+ * All Rights Reserved.
+ * The code is distributed under GPLv3 license, see LICENSE file for details.
+ * Generated by Cypher Stack on 2023-05-26
+ *
+ */
+
+import 'package:stackwallet/exceptions/sw_exception.dart';
+
+class JsonRpcException implements SWException {
+ JsonRpcException(this.message);
+
+ @override
+ final String message;
+
+ @override
+ toString() => message;
+}
diff --git a/lib/main.dart b/lib/main.dart
index 94c70c8b1..9d9b3e879 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -16,7 +16,6 @@ import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_libmonero/monero/monero.dart';
@@ -59,6 +58,7 @@ import 'package:stackwallet/services/locale_service.dart';
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/services/notifications_service.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/services/trade_service.dart';
import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/themes/theme_service.dart';
@@ -97,7 +97,7 @@ void main() async {
setWindowMaxSize(Size.infinite);
final screenHeight = screen?.frame.height;
- if (screenHeight != null && !kDebugMode) {
+ if (screenHeight != null) {
// starting to height be 3/4 screen height or 900, whichever is smaller
final height = min(screenHeight * 0.75, 900);
setWindowFrame(
@@ -167,6 +167,16 @@ void main() async {
await Hive.openBox(DB.boxNamePrefs);
await Prefs.instance.init();
+ // TODO:
+ // This should be moved to happen during the loading animation instead of
+ // showing a blank screen for 4-10 seconds.
+ // Some refactoring will need to be done here to make sure we don't make any
+ // network calls before starting up tor
+ if (Prefs.instance.useTor) {
+ TorService.sharedInstance.init();
+ await TorService.sharedInstance.start();
+ }
+
await StackFileSystem.initThemesDir();
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
diff --git a/lib/networking/http.dart b/lib/networking/http.dart
new file mode 100644
index 000000000..ad15b659c
--- /dev/null
+++ b/lib/networking/http.dart
@@ -0,0 +1,124 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:socks5_proxy/socks_client.dart';
+import 'package:stackwallet/utilities/logger.dart';
+
+// WIP wrapper layer
+
+// TODO expand this class
+class Response {
+ final int code;
+ final List bodyBytes;
+
+ String get body => utf8.decode(bodyBytes, allowMalformed: true);
+
+ Response(this.bodyBytes, this.code);
+}
+
+class HTTP {
+ Future get({
+ required Uri url,
+ Map? headers,
+ required ({
+ InternetAddress host,
+ int port,
+ })? proxyInfo,
+ }) async {
+ final httpClient = HttpClient();
+ try {
+ if (proxyInfo != null) {
+ SocksTCPClient.assignToHttpClient(httpClient, [
+ ProxySettings(
+ proxyInfo.host,
+ proxyInfo.port,
+ ),
+ ]);
+ }
+ final HttpClientRequest request = await httpClient.getUrl(
+ url,
+ );
+
+ if (headers != null) {
+ headers.forEach((key, value) => request.headers.add(key, value));
+ }
+
+ final response = await request.close();
+
+ return Response(
+ await _bodyBytes(response),
+ response.statusCode,
+ );
+ } catch (e, s) {
+ Logging.instance.log(
+ "HTTP.get() rethrew: $e\n$s",
+ level: LogLevel.Info,
+ );
+ rethrow;
+ } finally {
+ httpClient.close(force: true);
+ }
+ }
+
+ Future post({
+ required Uri url,
+ Map? headers,
+ Object? body,
+ Encoding? encoding,
+ required ({
+ InternetAddress host,
+ int port,
+ })? proxyInfo,
+ }) async {
+ final httpClient = HttpClient();
+ try {
+ if (proxyInfo != null) {
+ SocksTCPClient.assignToHttpClient(httpClient, [
+ ProxySettings(
+ proxyInfo.host,
+ proxyInfo.port,
+ ),
+ ]);
+ }
+ final HttpClientRequest request = await httpClient.postUrl(
+ url,
+ );
+
+ if (headers != null) {
+ headers.forEach((key, value) => request.headers.add(key, value));
+ }
+
+ request.write(body);
+
+ final response = await request.close();
+ return Response(
+ await _bodyBytes(response),
+ response.statusCode,
+ );
+ } catch (e, s) {
+ Logging.instance.log(
+ "HTTP.post() rethrew: $e\n$s",
+ level: LogLevel.Info,
+ );
+ rethrow;
+ } finally {
+ httpClient.close(force: true);
+ }
+ }
+
+ Future _bodyBytes(HttpClientResponse response) {
+ final completer = Completer();
+ final List bytes = [];
+ response.listen(
+ (data) {
+ bytes.addAll(data);
+ },
+ onDone: () => completer.complete(
+ Uint8List.fromList(bytes),
+ ),
+ );
+ return completer.future;
+ }
+}
diff --git a/lib/networking/socks_socket.dart b/lib/networking/socks_socket.dart
new file mode 100644
index 000000000..03dc60945
--- /dev/null
+++ b/lib/networking/socks_socket.dart
@@ -0,0 +1,343 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+/// A SOCKS5 socket.
+///
+/// This class is a wrapper around a Socket that connects to a SOCKS5 proxy
+/// server and sends all data through the proxy.
+///
+/// This class is used to connect to the Tor proxy server.
+///
+/// Attributes:
+/// - [proxyHost]: The host of the SOCKS5 proxy server.
+/// - [proxyPort]: The port of the SOCKS5 proxy server.
+/// - [_socksSocket]: The underlying [Socket] that connects to the SOCKS5 proxy
+/// server.
+/// - [_responseController]: A [StreamController] that listens to the
+/// [_socksSocket] and broadcasts the response.
+///
+/// Methods:
+/// - connect: Connects to the SOCKS5 proxy server.
+/// - connectTo: Connects to the specified [domain] and [port] through the
+/// SOCKS5 proxy server.
+/// - write: Converts [object] to a String by invoking [Object.toString] and
+/// sends the encoding of the result to the socket.
+/// - sendServerFeaturesCommand: Sends the server.features command to the
+/// proxy server.
+/// - close: Closes the connection to the Tor proxy.
+///
+/// Usage:
+/// ```dart
+/// // Instantiate a socks socket at localhost and on the port selected by the
+/// // tor service.
+/// var socksSocket = await SOCKSSocket.create(
+/// proxyHost: InternetAddress.loopbackIPv4.address,
+/// proxyPort: tor.port,
+/// // sslEnabled: true, // For SSL connections.
+/// );
+///
+/// // Connect to the socks instantiated above.
+/// await socksSocket.connect();
+///
+/// // Connect to bitcoincash.stackwallet.com on port 50001 via socks socket.
+/// await socksSocket.connectTo(
+/// 'bitcoincash.stackwallet.com', 50001);
+///
+/// // Send a server features command to the connected socket, see method for
+/// // more specific usage example..
+/// await socksSocket.sendServerFeaturesCommand();
+/// await socksSocket.close();
+/// ```
+///
+/// See also:
+/// - SOCKS5 protocol(https://www.ietf.org/rfc/rfc1928.txt)
+class SOCKSSocket {
+ /// The host of the SOCKS5 proxy server.
+ final String proxyHost;
+
+ /// The port of the SOCKS5 proxy server.
+ final int proxyPort;
+
+ /// The underlying Socket that connects to the SOCKS5 proxy server.
+ late final Socket _socksSocket;
+
+ /// Getter for the underlying Socket that connects to the SOCKS5 proxy server.
+ Socket get socket => sslEnabled ? _secureSocksSocket : _socksSocket;
+
+ /// A wrapper around the _socksSocket that enables SSL connections.
+ late final Socket _secureSocksSocket;
+
+ /// A StreamController that listens to the _socksSocket and broadcasts.
+ final StreamController> _responseController =
+ StreamController.broadcast();
+
+ /// A StreamController that listens to the _secureSocksSocket and broadcasts.
+ final StreamController> _secureResponseController =
+ StreamController.broadcast();
+
+ /// Getter for the StreamController that listens to the _socksSocket and
+ /// broadcasts, or the _secureSocksSocket and broadcasts if SSL is enabled.
+ StreamController> get responseController =>
+ sslEnabled ? _secureResponseController : _responseController;
+
+ /// A StreamSubscription that listens to the _socksSocket or the
+ /// _secureSocksSocket if SSL is enabled.
+ StreamSubscription>? _subscription;
+
+ /// Getter for the StreamSubscription that listens to the _socksSocket or the
+ /// _secureSocksSocket if SSL is enabled.
+ StreamSubscription>? get subscription => _subscription;
+
+ /// Is SSL enabled?
+ final bool sslEnabled;
+
+ /// Private constructor.
+ SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled);
+
+ /// Creates a SOCKS5 socket to the specified [proxyHost] and [proxyPort].
+ ///
+ /// This method is a factory constructor that returns a Future that resolves
+ /// to a SOCKSSocket instance.
+ ///
+ /// Parameters:
+ /// - [proxyHost]: The host of the SOCKS5 proxy server.
+ /// - [proxyPort]: The port of the SOCKS5 proxy server.
+ ///
+ /// Returns:
+ /// A Future that resolves to a SOCKSSocket instance.
+ static Future create(
+ {required String proxyHost,
+ required int proxyPort,
+ bool sslEnabled = false}) async {
+ // Create a SOCKS socket instance.
+ var instance = SOCKSSocket._(proxyHost, proxyPort, sslEnabled);
+
+ // Initialize the SOCKS socket.
+ await instance._init();
+
+ // Return the SOCKS socket instance.
+ return instance;
+ }
+
+ /// Constructor.
+ SOCKSSocket(
+ {required this.proxyHost,
+ required this.proxyPort,
+ required this.sslEnabled}) {
+ _init();
+ }
+
+ /// Initializes the SOCKS socket.
+ ///
+ /// This method is a private method that is called by the constructor.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ Future _init() async {
+ // Connect to the SOCKS proxy server.
+ _socksSocket = await Socket.connect(
+ proxyHost,
+ proxyPort,
+ );
+
+ // Listen to the socket.
+ _subscription = _socksSocket.listen(
+ (data) {
+ // Add the data to the response controller.
+ _responseController.add(data);
+ },
+ onError: (e) {
+ // Handle errors.
+ if (e is Object) {
+ _responseController.addError(e);
+ }
+
+ // If the error is not an object, send the error as a string.
+ _responseController.addError("$e");
+ // TODO make sure sending error as string is acceptable.
+ },
+ onDone: () {
+ // Close the response controller when the socket is closed.
+ _responseController.close();
+ },
+ );
+ }
+
+ /// Connects to the SOCKS socket.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ Future connect() async {
+ // Greeting and method selection.
+ _socksSocket.add([0x05, 0x01, 0x00]);
+
+ // Wait for server response.
+ var response = await _responseController.stream.first;
+
+ // Check if the connection was successful.
+ if (response[1] != 0x00) {
+ throw Exception(
+ 'socks_socket.connect(): Failed to connect to SOCKS5 proxy.');
+ }
+ }
+
+ /// Connects to the specified [domain] and [port] through the SOCKS socket.
+ ///
+ /// Parameters:
+ /// - [domain]: The domain to connect to.
+ /// - [port]: The port to connect to.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ Future connectTo(String domain, int port) async {
+ // Connect command.
+ var request = [
+ 0x05, // SOCKS version.
+ 0x01, // Connect command.
+ 0x00, // Reserved.
+ 0x03, // Domain name.
+ domain.length,
+ ...domain.codeUnits,
+ (port >> 8) & 0xFF,
+ port & 0xFF
+ ];
+
+ // Send the connect command to the SOCKS proxy server.
+ _socksSocket.add(request);
+
+ // Wait for server response.
+ var response = await _responseController.stream.first;
+
+ // Check if the connection was successful.
+ if (response[1] != 0x00) {
+ throw Exception(
+ 'socks_socket.connectTo(): Failed to connect to target through SOCKS5 proxy.');
+ }
+
+ // Upgrade to SSL if needed
+ if (sslEnabled) {
+ // Upgrade to SSL.
+ _secureSocksSocket = await SecureSocket.secure(
+ _socksSocket,
+ host: domain,
+ // onBadCertificate: (_) => true, // Uncomment this to bypass certificate validation (NOT recommended for production).
+ );
+
+ // Listen to the secure socket.
+ _subscription = _secureSocksSocket.listen(
+ (data) {
+ // Add the data to the response controller.
+ _secureResponseController.add(data);
+ },
+ onError: (e) {
+ // Handle errors.
+ if (e is Object) {
+ _secureResponseController.addError(e);
+ }
+
+ // If the error is not an object, send the error as a string.
+ _secureResponseController.addError("$e");
+ // TODO make sure sending error as string is acceptable.
+ },
+ onDone: () {
+ // Close the response controller when the socket is closed.
+ _secureResponseController.close();
+ },
+ );
+ }
+
+ return;
+ }
+
+ /// Converts [object] to a String by invoking [Object.toString] and
+ /// sends the encoding of the result to the socket.
+ ///
+ /// Parameters:
+ /// - [object]: The object to write to the socket.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ void write(Object? object) {
+ // Don't write null.
+ if (object == null) return;
+
+ // Write the data to the socket.
+ List data = utf8.encode(object.toString());
+ if (sslEnabled) {
+ _secureSocksSocket.add(data);
+ } else {
+ _socksSocket.add(data);
+ }
+ }
+
+ /// Sends the server.features command to the proxy server.
+ ///
+ /// This method is used to send the server.features command to the proxy
+ /// server. This command is used to request the features of the proxy server.
+ /// It serves as a demonstration of how to send commands to the proxy server.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ Future sendServerFeaturesCommand() async {
+ // The server.features command.
+ const String command =
+ '{"jsonrpc":"2.0","id":"0","method":"server.features","params":[]}';
+
+ if (!sslEnabled) {
+ // Send the command to the proxy server.
+ _socksSocket.writeln(command);
+
+ // Wait for the response from the proxy server.
+ var responseData = await _responseController.stream.first;
+ print("responseData: ${utf8.decode(responseData)}");
+ } else {
+ // Send the command to the proxy server.
+ _secureSocksSocket.writeln(command);
+
+ // Wait for the response from the proxy server.
+ var responseData = await _secureResponseController.stream.first;
+ print("secure responseData: ${utf8.decode(responseData)}");
+ }
+
+ return;
+ }
+
+ /// Closes the connection to the Tor proxy.
+ ///
+ /// Returns:
+ /// A Future that resolves to void.
+ Future close() async {
+ // Ensure all data is sent before closing.
+ //
+ // TODO test this.
+ if (sslEnabled) {
+ await _socksSocket.flush();
+ await _secureResponseController.close();
+ }
+ await _socksSocket.flush();
+ await _responseController.close();
+ return await _socksSocket.close();
+ }
+
+ StreamSubscription> listen(
+ void Function(List data)? onData, {
+ Function? onError,
+ void Function()? onDone,
+ bool? cancelOnError,
+ }) {
+ return sslEnabled
+ ? _secureResponseController.stream.listen(
+ onData,
+ onError: onError,
+ onDone: onDone,
+ cancelOnError: cancelOnError,
+ )
+ : _responseController.stream.listen(
+ onData,
+ onError: onError,
+ onDone: onDone,
+ cancelOnError: cancelOnError,
+ );
+ }
+}
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 be5aaa37d..e6d9149a7 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
@@ -8,7 +8,10 @@
*
*/
+import 'dart:io';
+
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
@@ -16,6 +19,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
@@ -30,16 +34,17 @@ class AddTokenListElementData {
bool selected = false;
}
-class AddTokenListElement extends StatefulWidget {
+class AddTokenListElement extends ConsumerStatefulWidget {
const AddTokenListElement({Key? key, required this.data}) : super(key: key);
final AddTokenListElementData data;
@override
- State createState() => _AddTokenListElementState();
+ ConsumerState createState() =>
+ _AddTokenListElementState();
}
-class _AddTokenListElementState extends State {
+class _AddTokenListElementState extends ConsumerState {
final bool isDesktop = Util.isDesktop;
@override
@@ -74,6 +79,17 @@ class _AddTokenListElementState extends State {
currency.image,
width: iconSize,
height: iconSize,
+ placeholderBuilder: (_) => SvgPicture.file(
+ File(
+ ref.watch(
+ themeAssetsProvider.select(
+ (value) => value.stackIcon,
+ ),
+ ),
+ ),
+ width: iconSize,
+ height: iconSize,
+ ),
)
: SvgPicture.asset(
widget.data.token.symbol == "BNB"
diff --git a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart
index 4e04fcb80..f07941e36 100644
--- a/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart
+++ b/lib/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart
@@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@@ -336,46 +337,60 @@ class _NameYourWalletViewState extends ConsumerState {
ref.read(walletsServiceChangeNotifierProvider);
final name = textEditingController.text;
- if (await walletsService.checkForDuplicate(name)) {
- unawaited(showFloatingFlushBar(
- type: FlushBarType.warning,
- message: "Wallet name already in use.",
- iconAsset: Assets.svg.circleAlert,
- context: context,
- ));
- } else {
- // hide keyboard if has focus
- if (FocusScope.of(context).hasFocus) {
- FocusScope.of(context).unfocus();
- await Future.delayed(
- const Duration(milliseconds: 50));
- }
+ final hasDuplicateName =
+ await walletsService.checkForDuplicate(name);
- if (mounted) {
- switch (widget.addWalletType) {
- case AddWalletType.New:
- unawaited(Navigator.of(context).pushNamed(
- NewWalletRecoveryPhraseWarningView.routeName,
- arguments: Tuple2(
- name,
- coin,
- ),
- ));
- break;
- case AddWalletType.Restore:
- ref
- .read(mnemonicWordCountStateProvider.state)
- .state = Constants.possibleLengthsForCoin(
- coin)
- .first;
- unawaited(Navigator.of(context).pushNamed(
- RestoreOptionsView.routeName,
- arguments: Tuple2(
- name,
- coin,
- ),
- ));
- break;
+ if (mounted) {
+ if (hasDuplicateName) {
+ unawaited(showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: "Wallet name already in use.",
+ iconAsset: Assets.svg.circleAlert,
+ context: context,
+ ));
+ } else {
+ // hide keyboard if has focus
+ if (FocusScope.of(context).hasFocus) {
+ FocusScope.of(context).unfocus();
+ await Future.delayed(
+ const Duration(milliseconds: 50));
+ }
+
+ if (mounted) {
+ ref
+ .read(mnemonicWordCountStateProvider.state)
+ .state =
+ Constants.possibleLengthsForCoin(coin).last;
+ ref.read(pNewWalletOptions.notifier).state = null;
+
+ switch (widget.addWalletType) {
+ case AddWalletType.New:
+ unawaited(
+ Navigator.of(context).pushNamed(
+ coin.hasMnemonicPassphraseSupport
+ ? NewWalletOptionsView.routeName
+ : NewWalletRecoveryPhraseWarningView
+ .routeName,
+ arguments: Tuple2(
+ name,
+ coin,
+ ),
+ ),
+ );
+ break;
+
+ case AddWalletType.Restore:
+ unawaited(
+ Navigator.of(context).pushNamed(
+ RestoreOptionsView.routeName,
+ arguments: Tuple2(
+ name,
+ coin,
+ ),
+ ),
+ );
+ break;
+ }
}
}
}
diff --git a/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart b/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart
new file mode 100644
index 000000000..f6d3075b2
--- /dev/null
+++ b/lib/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart
@@ -0,0 +1,410 @@
+import 'package:dropdown_button2/dropdown_button2.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
+import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/sub_widgets/mobile_mnemonic_length_selector.dart';
+import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/mnemonic_word_count_select_sheet.dart';
+import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
+import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.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/utilities/util.dart';
+import 'package:stackwallet/widgets/background.dart';
+import 'package:stackwallet/widgets/conditional_parent.dart';
+import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
+import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
+import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
+import 'package:stackwallet/widgets/desktop/primary_button.dart';
+import 'package:stackwallet/widgets/rounded_white_container.dart';
+import 'package:stackwallet/widgets/stack_text_field.dart';
+import 'package:tuple/tuple.dart';
+
+final pNewWalletOptions =
+ StateProvider<({String mnemonicPassphrase, int mnemonicWordsCount})?>(
+ (ref) => null);
+
+enum NewWalletOptions {
+ Default,
+ Advanced;
+}
+
+class NewWalletOptionsView extends ConsumerStatefulWidget {
+ const NewWalletOptionsView({
+ Key? key,
+ required this.walletName,
+ required this.coin,
+ }) : super(key: key);
+
+ static const routeName = "/newWalletOptionsView";
+
+ final String walletName;
+ final Coin coin;
+
+ @override
+ ConsumerState createState() =>
+ _NewWalletOptionsViewState();
+}
+
+class _NewWalletOptionsViewState extends ConsumerState {
+ late final FocusNode passwordFocusNode;
+ late final TextEditingController passwordController;
+
+ bool hidePassword = true;
+ NewWalletOptions _selectedOptions = NewWalletOptions.Default;
+
+ @override
+ void initState() {
+ passwordController = TextEditingController();
+ passwordFocusNode = FocusNode();
+
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ passwordController.dispose();
+ passwordFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final lengths = Constants.possibleLengthsForCoin(widget.coin).toList();
+ return ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => DesktopScaffold(
+ background: Theme.of(context).extension()!.background,
+ appBar: const DesktopAppBar(
+ isCompactHeight: false,
+ leading: AppBarBackButton(),
+ trailing: ExitToMyStackButton(),
+ ),
+ body: SizedBox(
+ width: 480,
+ child: child,
+ ),
+ ),
+ child: ConditionalParent(
+ condition: !Util.isDesktop,
+ builder: (child) => Background(
+ child: Scaffold(
+ backgroundColor:
+ Theme.of(context).extension()!.background,
+ appBar: AppBar(
+ leading: const AppBarBackButton(),
+ title: Text(
+ "Wallet Options",
+ style: STextStyles.navBarTitle(context),
+ ),
+ ),
+ body: SafeArea(
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return SingleChildScrollView(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ minHeight: constraints.maxHeight,
+ ),
+ child: IntrinsicHeight(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: child,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ child: Column(
+ children: [
+ if (Util.isDesktop)
+ const Spacer(
+ flex: 10,
+ ),
+ if (!Util.isDesktop)
+ const SizedBox(
+ height: 16,
+ ),
+ if (!Util.isDesktop)
+ CoinImage(
+ coin: widget.coin,
+ height: 100,
+ width: 100,
+ ),
+ if (Util.isDesktop)
+ Text(
+ "Wallet options",
+ textAlign: TextAlign.center,
+ style: Util.isDesktop
+ ? STextStyles.desktopH2(context)
+ : STextStyles.pageTitleH1(context),
+ ),
+ SizedBox(
+ height: Util.isDesktop ? 32 : 16,
+ ),
+ DropdownButtonHideUnderline(
+ child: DropdownButton2(
+ value: _selectedOptions,
+ items: [
+ ...NewWalletOptions.values.map(
+ (e) => DropdownMenuItem(
+ value: e,
+ child: Text(
+ e.name,
+ style: STextStyles.desktopTextMedium(context),
+ ),
+ ),
+ ),
+ ],
+ onChanged: (value) {
+ if (value is NewWalletOptions) {
+ setState(() {
+ _selectedOptions = value;
+ });
+ }
+ },
+ isExpanded: true,
+ iconStyleData: IconStyleData(
+ icon: SvgPicture.asset(
+ Assets.svg.chevronDown,
+ width: 12,
+ height: 6,
+ color: Theme.of(context)
+ .extension()!
+ .textFieldActiveSearchIconRight,
+ ),
+ ),
+ dropdownStyleData: DropdownStyleData(
+ offset: const Offset(0, -10),
+ elevation: 0,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .extension()!
+ .textFieldDefaultBG,
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ ),
+ menuItemStyleData: const MenuItemStyleData(
+ padding: EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(
+ height: 24,
+ ),
+ if (_selectedOptions == NewWalletOptions.Advanced)
+ Column(
+ children: [
+ if (Util.isDesktop)
+ DropdownButtonHideUnderline(
+ child: DropdownButton2(
+ value: ref
+ .watch(mnemonicWordCountStateProvider.state)
+ .state,
+ items: [
+ ...lengths.map(
+ (e) => DropdownMenuItem(
+ value: e,
+ child: Text(
+ "$e word seed",
+ style: STextStyles.desktopTextMedium(context),
+ ),
+ ),
+ ),
+ ],
+ onChanged: (value) {
+ if (value is int) {
+ ref
+ .read(mnemonicWordCountStateProvider.state)
+ .state = value;
+ }
+ },
+ isExpanded: true,
+ iconStyleData: IconStyleData(
+ icon: SvgPicture.asset(
+ Assets.svg.chevronDown,
+ width: 12,
+ height: 6,
+ color: Theme.of(context)
+ .extension()!
+ .textFieldActiveSearchIconRight,
+ ),
+ ),
+ dropdownStyleData: DropdownStyleData(
+ offset: const Offset(0, -10),
+ elevation: 0,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .extension()!
+ .textFieldDefaultBG,
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ ),
+ menuItemStyleData: const MenuItemStyleData(
+ padding: EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 8,
+ ),
+ ),
+ ),
+ ),
+ if (!Util.isDesktop)
+ MobileMnemonicLengthSelector(
+ chooseMnemonicLength: () {
+ showModalBottomSheet(
+ backgroundColor: Colors.transparent,
+ context: context,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(20),
+ ),
+ ),
+ builder: (_) {
+ return MnemonicWordCountSelectSheet(
+ lengthOptions: lengths,
+ );
+ },
+ );
+ },
+ ),
+ const SizedBox(
+ height: 24,
+ ),
+ RoundedWhiteContainer(
+ child: Center(
+ child: Text(
+ "You may add a BIP39 passphrase. This is optional. "
+ "You will need BOTH your seed and your passphrase to recover the wallet.",
+ style: Util.isDesktop
+ ? STextStyles.desktopTextExtraSmall(context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .textSubtitle1,
+ )
+ : STextStyles.itemSubtitle(context),
+ ),
+ ),
+ ),
+ const SizedBox(
+ height: 8,
+ ),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ key: const Key("mnemonicPassphraseFieldKey1"),
+ focusNode: passwordFocusNode,
+ controller: passwordController,
+ style: Util.isDesktop
+ ? STextStyles.desktopTextMedium(context).copyWith(
+ height: 2,
+ )
+ : STextStyles.field(context),
+ obscureText: hidePassword,
+ enableSuggestions: false,
+ autocorrect: false,
+ decoration: standardInputDecoration(
+ "BIP39 passphrase",
+ passwordFocusNode,
+ context,
+ ).copyWith(
+ suffixIcon: UnconstrainedBox(
+ child: ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => SizedBox(
+ height: 70,
+ child: child,
+ ),
+ child: Row(
+ children: [
+ SizedBox(
+ width: Util.isDesktop ? 24 : 16,
+ ),
+ GestureDetector(
+ key: const Key(
+ "mnemonicPassphraseFieldShowPasswordButtonKey"),
+ onTap: () async {
+ setState(() {
+ hidePassword = !hidePassword;
+ });
+ },
+ child: SvgPicture.asset(
+ hidePassword
+ ? Assets.svg.eye
+ : Assets.svg.eyeSlash,
+ color: Theme.of(context)
+ .extension()!
+ .textDark3,
+ width: Util.isDesktop ? 24 : 16,
+ height: Util.isDesktop ? 24 : 16,
+ ),
+ ),
+ const SizedBox(
+ width: 12,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (!Util.isDesktop) const Spacer(),
+ SizedBox(
+ height: Util.isDesktop ? 32 : 16,
+ ),
+ PrimaryButton(
+ label: "Continue",
+ onPressed: () {
+ if (_selectedOptions == NewWalletOptions.Advanced) {
+ ref.read(pNewWalletOptions.notifier).state = (
+ mnemonicWordsCount:
+ ref.read(mnemonicWordCountStateProvider.state).state,
+ mnemonicPassphrase: passwordController.text,
+ );
+ } else {
+ ref.read(pNewWalletOptions.notifier).state = null;
+ }
+
+ Navigator.of(context).pushNamed(
+ NewWalletRecoveryPhraseWarningView.routeName,
+ arguments: Tuple2(
+ widget.walletName,
+ widget.coin,
+ ),
+ );
+ },
+ ),
+ if (!Util.isDesktop)
+ const SizedBox(
+ height: 16,
+ ),
+ if (Util.isDesktop)
+ const Spacer(
+ flex: 15,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
index e373e817b..d843f6ec8 100644
--- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
+++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart
@@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/recovery_phrase_explanation_dialog.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@@ -38,7 +39,7 @@ import 'package:stackwallet/widgets/rounded_container.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart';
-class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
+class NewWalletRecoveryPhraseWarningView extends ConsumerStatefulWidget {
const NewWalletRecoveryPhraseWarningView({
Key? key,
required this.coin,
@@ -51,12 +52,12 @@ class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
final String walletName;
@override
- State createState() =>
+ ConsumerState createState() =>
_NewWalletRecoveryPhraseWarningViewState();
}
class _NewWalletRecoveryPhraseWarningViewState
- extends State {
+ extends ConsumerState {
late final Coin coin;
late final String walletName;
late final bool isDesktop;
@@ -72,6 +73,10 @@ class _NewWalletRecoveryPhraseWarningViewState
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
+ final options = ref.read(pNewWalletOptions.state).state;
+
+ final seedCount = options?.mnemonicWordsCount ??
+ Constants.defaultSeedPhraseLengthFor(coin: coin);
return MasterScaffold(
isDesktop: isDesktop,
@@ -172,7 +177,7 @@ class _NewWalletRecoveryPhraseWarningViewState
child: isDesktop
? Text(
"On the next screen you will see "
- "${Constants.defaultSeedPhraseLengthFor(coin: coin)} "
+ "$seedCount "
"words that make up your recovery phrase.\n\nPlease "
"write it down. Keep it safe and never share it with "
"anyone. Your recovery phrase is the only way you can"
@@ -216,9 +221,7 @@ class _NewWalletRecoveryPhraseWarningViewState
),
),
TextSpan(
- text:
- "${Constants.defaultSeedPhraseLengthFor(coin: coin)}"
- " words",
+ text: "$seedCount words",
style: STextStyles.desktopH3(context).copyWith(
color: Theme.of(context)
.extension()!
@@ -496,7 +499,24 @@ class _NewWalletRecoveryPhraseWarningViewState
final manager = Manager(wallet);
- await manager.initializeNew();
+ if (coin.hasMnemonicPassphraseSupport &&
+ ref
+ .read(pNewWalletOptions.state)
+ .state !=
+ null) {
+ await manager.initializeNew((
+ mnemonicPassphrase: ref
+ .read(pNewWalletOptions.state)
+ .state!
+ .mnemonicPassphrase,
+ wordCount: ref
+ .read(pNewWalletOptions.state)
+ .state!
+ .mnemonicWordsCount,
+ ));
+ } else {
+ await manager.initializeNew(null);
+ }
// pop progress dialog
if (mounted) {
diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart
index 59d1c0a8f..44ac51aac 100644
--- a/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart
+++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart
@@ -535,7 +535,7 @@ class _RestoreOptionsViewState extends ConsumerState {
enableSuggestions: false,
autocorrect: false,
decoration: standardInputDecoration(
- "Recovery phrase password",
+ "BIP39 passphrase",
passwordFocusNode,
context,
).copyWith(
@@ -586,7 +586,9 @@ class _RestoreOptionsViewState extends ConsumerState {
RoundedWhiteContainer(
child: Center(
child: Text(
- "If the recovery phrase you are about to restore was created with an optional passphrase you can enter it here.",
+ "If the recovery phrase you are about to restore "
+ "was created with an optional BIP39 passphrase "
+ "you can enter it here.",
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
index 87219d277..1a62af746 100644
--- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
+++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
@@ -98,7 +98,7 @@ class _RestoreWalletViewState extends ConsumerState {
final List _controllers = [];
final List _inputStatuses = [];
- final List _focusNodes = [];
+ // final List _focusNodes = [];
late final BarcodeScannerInterface scanner;
@@ -152,7 +152,7 @@ class _RestoreWalletViewState extends ConsumerState {
for (int i = 0; i < _seedWordCount; i++) {
_controllers.add(TextEditingController());
_inputStatuses.add(FormInputStatus.empty);
- _focusNodes.add(FocusNode());
+ // _focusNodes.add(FocusNode());
}
super.initState();
@@ -821,8 +821,8 @@ class _RestoreWalletViewState extends ConsumerState {
i * 4 + j - 1 == 1
? textSelectionControls
: null,
- focusNode:
- _focusNodes[i * 4 + j - 1],
+ // focusNode:
+ // _focusNodes[i * 4 + j - 1],
onChanged: (value) {
final FormInputStatus
formInputStatus;
@@ -841,18 +841,18 @@ class _RestoreWalletViewState extends ConsumerState {
FormInputStatus.invalid;
}
- if (formInputStatus ==
- FormInputStatus.valid) {
- if (i * 4 + j <
- _focusNodes.length) {
- _focusNodes[i * 4 + j]
- .requestFocus();
- } else if (i * 4 + j ==
- _focusNodes.length) {
- _focusNodes[i * 4 + j - 1]
- .unfocus();
- }
- }
+ // if (formInputStatus ==
+ // FormInputStatus.valid) {
+ // if (i * 4 + j <
+ // _focusNodes.length) {
+ // _focusNodes[i * 4 + j]
+ // .requestFocus();
+ // } else if (i * 4 + j ==
+ // _focusNodes.length) {
+ // _focusNodes[i * 4 + j - 1]
+ // .unfocus();
+ // }
+ // }
setState(() {
_inputStatuses[i * 4 +
j -
@@ -929,7 +929,7 @@ class _RestoreWalletViewState extends ConsumerState {
selectionControls: i == 1
? textSelectionControls
: null,
- focusNode: _focusNodes[i],
+ // focusNode: _focusNodes[i],
onChanged: (value) {
final FormInputStatus
formInputStatus;
@@ -948,27 +948,27 @@ class _RestoreWalletViewState extends ConsumerState {
FormInputStatus.invalid;
}
- if (formInputStatus ==
- FormInputStatus
- .valid &&
- (i - 1) <
- _focusNodes.length) {
- Focus.of(context)
- .requestFocus(
- _focusNodes[i]);
- }
+ // if (formInputStatus ==
+ // FormInputStatus
+ // .valid &&
+ // (i - 1) <
+ // _focusNodes.length) {
+ // Focus.of(context)
+ // .requestFocus(
+ // _focusNodes[i]);
+ // }
- if (formInputStatus ==
- FormInputStatus.valid) {
- if (i + 1 <
- _focusNodes.length) {
- _focusNodes[i + 1]
- .requestFocus();
- } else if (i + 1 ==
- _focusNodes.length) {
- _focusNodes[i].unfocus();
- }
- }
+ // if (formInputStatus ==
+ // FormInputStatus.valid) {
+ // if (i + 1 <
+ // _focusNodes.length) {
+ // _focusNodes[i + 1]
+ // .requestFocus();
+ // } else if (i + 1 ==
+ // _focusNodes.length) {
+ // _focusNodes[i].unfocus();
+ // }
+ // }
},
controller: _controllers[i],
style:
@@ -1068,7 +1068,7 @@ class _RestoreWalletViewState extends ConsumerState {
AutovalidateMode.onUserInteraction,
selectionControls:
i == 1 ? textSelectionControls : null,
- focusNode: _focusNodes[i - 1],
+ // focusNode: _focusNodes[i - 1],
onChanged: (value) {
final FormInputStatus formInputStatus;
@@ -1084,14 +1084,14 @@ class _RestoreWalletViewState extends ConsumerState {
FormInputStatus.invalid;
}
- if (formInputStatus ==
- FormInputStatus.valid) {
- if (i < _focusNodes.length) {
- _focusNodes[i].requestFocus();
- } else if (i == _focusNodes.length) {
- _focusNodes[i - 1].unfocus();
- }
- }
+ // if (formInputStatus ==
+ // FormInputStatus.valid) {
+ // if (i < _focusNodes.length) {
+ // _focusNodes[i].requestFocus();
+ // } else if (i == _focusNodes.length) {
+ // _focusNodes[i - 1].unfocus();
+ // }
+ // }
setState(() {
_inputStatuses[i - 1] =
formInputStatus;
diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart
new file mode 100644
index 000000000..bae25a7e1
--- /dev/null
+++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart
@@ -0,0 +1,218 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/notifications/show_flush_bar.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/utilities/util.dart';
+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/desktop/primary_button.dart';
+import 'package:stackwallet/widgets/desktop/secondary_button.dart';
+import 'package:stackwallet/widgets/stack_dialog.dart';
+import 'package:stackwallet/widgets/stack_text_field.dart';
+
+class VerifyMnemonicPassphraseDialog extends ConsumerStatefulWidget {
+ const VerifyMnemonicPassphraseDialog({super.key});
+
+ @override
+ ConsumerState createState() =>
+ _VerifyMnemonicPassphraseDialogState();
+}
+
+class _VerifyMnemonicPassphraseDialogState
+ extends ConsumerState {
+ late final FocusNode passwordFocusNode;
+ late final TextEditingController passwordController;
+
+ bool hidePassword = true;
+
+ bool _verifyLock = false;
+
+ void _verify() {
+ if (_verifyLock) {
+ return;
+ }
+ _verifyLock = true;
+
+ if (passwordController.text ==
+ ref.read(pNewWalletOptions.state).state!.mnemonicPassphrase) {
+ Navigator.of(context, rootNavigator: Util.isDesktop).pop("verified");
+ } else {
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: "Passphrase does not match",
+ context: context,
+ );
+ }
+
+ _verifyLock = false;
+ }
+
+ @override
+ void initState() {
+ passwordController = TextEditingController();
+ passwordFocusNode = FocusNode();
+
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ passwordController.dispose();
+ passwordFocusNode.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => DesktopDialog(
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ ),
+ child: Text(
+ "Verify mnemonic passphrase",
+ style: STextStyles.desktopH3(context),
+ ),
+ ),
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 32,
+ right: 32,
+ bottom: 32,
+ ),
+ child: child,
+ ),
+ ],
+ ),
+ ),
+ child: ConditionalParent(
+ condition: !Util.isDesktop,
+ builder: (child) => StackDialogBase(
+ keyboardPaddingAmount: MediaQuery.of(context).viewInsets.bottom,
+ child: child,
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (!Util.isDesktop)
+ Text(
+ "Verify BIP39 passphrase",
+ style: STextStyles.pageTitleH2(context),
+ ),
+ const SizedBox(
+ height: 24,
+ ),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ key: const Key("mnemonicPassphraseFieldKey1"),
+ focusNode: passwordFocusNode,
+ controller: passwordController,
+ style: Util.isDesktop
+ ? STextStyles.desktopTextMedium(context).copyWith(
+ height: 2,
+ )
+ : STextStyles.field(context),
+ obscureText: hidePassword,
+ enableSuggestions: false,
+ autocorrect: false,
+ decoration: standardInputDecoration(
+ "Enter your BIP39 passphrase",
+ passwordFocusNode,
+ context,
+ ).copyWith(
+ suffixIcon: UnconstrainedBox(
+ child: ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => SizedBox(
+ height: 70,
+ child: child,
+ ),
+ child: Row(
+ children: [
+ SizedBox(
+ width: Util.isDesktop ? 24 : 16,
+ ),
+ GestureDetector(
+ key: const Key(
+ "mnemonicPassphraseFieldShowPasswordButtonKey"),
+ onTap: () async {
+ setState(() {
+ hidePassword = !hidePassword;
+ });
+ },
+ child: SvgPicture.asset(
+ hidePassword
+ ? Assets.svg.eye
+ : Assets.svg.eyeSlash,
+ color: Theme.of(context)
+ .extension()!
+ .textDark3,
+ width: Util.isDesktop ? 24 : 16,
+ height: Util.isDesktop ? 24 : 16,
+ ),
+ ),
+ const SizedBox(
+ width: 12,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ SizedBox(
+ height: Util.isDesktop ? 48 : 24,
+ ),
+ ConditionalParent(
+ condition: !Util.isDesktop,
+ builder: (child) => Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ onPressed: Navigator.of(
+ context,
+ rootNavigator: Util.isDesktop,
+ ).pop,
+ ),
+ ),
+ const SizedBox(
+ width: 16,
+ ),
+ Expanded(
+ child: child,
+ ),
+ ],
+ ),
+ child: PrimaryButton(
+ label: "Verify",
+ onPressed: _verify,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart
index fda4419d9..0d0083cc6 100644
--- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart
+++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart
@@ -16,9 +16,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart';
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart';
+import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart';
import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@@ -98,8 +100,25 @@ class _VerifyRecoveryPhraseViewState
// }
// }
+ Future _verifyMnemonicPassphrase() async {
+ final result = await showDialog(
+ context: context,
+ builder: (_) => const VerifyMnemonicPassphraseDialog(),
+ );
+
+ return result == "verified";
+ }
+
Future _continue(bool isMatch) async {
if (isMatch) {
+ if (ref.read(pNewWalletOptions.state).state != null) {
+ final passphraseVerified = await _verifyMnemonicPassphrase();
+
+ if (!passphraseVerified) {
+ return;
+ }
+ }
+
await ref.read(walletsServiceChangeNotifierProvider).setMnemonicVerified(
walletId: _manager.walletId,
);
diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart
index 3d5fe1539..beeece754 100644
--- a/lib/pages/buy_view/buy_view.dart
+++ b/lib/pages/buy_view/buy_view.dart
@@ -9,38 +9,82 @@
*/
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/pages/buy_view/buy_form.dart';
+import 'package:stackwallet/providers/global/prefs_provider.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
+import 'package:stackwallet/widgets/stack_dialog.dart';
-class BuyView extends StatelessWidget {
+class BuyView extends ConsumerStatefulWidget {
const BuyView({
Key? key,
this.coin,
this.tokenContract,
}) : super(key: key);
- static const String routeName = "/stackBuyView";
-
final Coin? coin;
final EthContract? tokenContract;
+ static const String routeName = "/stackBuyView";
+
+ @override
+ ConsumerState createState() => _BuyViewState();
+}
+
+class _BuyViewState extends ConsumerState {
+ Coin? coin;
+ EthContract? tokenContract;
+ late bool torEnabled = false;
+
+ @override
+ void initState() {
+ coin = widget.coin;
+ tokenContract = widget.tokenContract;
+
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ setState(() {
+ torEnabled = ref.read(prefsChangeNotifierProvider).useTor;
+ });
+ });
+
+ super.initState();
+ }
+
@override
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
- return SafeArea(
- child: Padding(
- padding: const EdgeInsets.only(
- left: 16,
- right: 16,
- top: 16,
+ return Stack(
+ children: [
+ SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 16,
+ right: 16,
+ top: 16,
+ ),
+ child: BuyForm(
+ coin: coin,
+ tokenContract: tokenContract,
+ ),
+ ),
),
- child: BuyForm(
- coin: coin,
- tokenContract: tokenContract,
- ),
- ),
+ if (torEnabled)
+ Container(
+ color: Theme.of(context)
+ .extension()!
+ .overlay
+ .withOpacity(0.7),
+ height: MediaQuery.of(context).size.height,
+ width: MediaQuery.of(context).size.width,
+ child: const StackDialog(
+ title: "Tor is enabled",
+ message: "Purchasing not available while Tor is enabled",
+ ),
+ ),
+ ],
);
}
}
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 87fc590e9..ada195455 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
@@ -24,6 +24,7 @@ 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/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
@@ -39,6 +40,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
+import '../../../services/exchange/exchange.dart';
+
class ExchangeCurrencySelectionView extends StatefulWidget {
const ExchangeCurrencySelectionView({
Key? key,
@@ -125,7 +128,7 @@ class _ExchangeCurrencySelectionViewState
await showDialog(
context: context,
builder: (context) => StackDialog(
- title: "ChangeNOW Error",
+ title: "Exchange Error",
message: "Failed to load currency data: ${cn.exception}",
leftButton: SecondaryButton(
label: "Ok",
@@ -169,6 +172,15 @@ class _ExchangeCurrencySelectionViewState
.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));
+ }
+ }
+
return _getDistinctCurrenciesFrom(currencies);
}
diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart
index a15e13843..a81f758a6 100644
--- a/lib/pages/exchange_view/exchange_form.dart
+++ b/lib/pages/exchange_view/exchange_form.dart
@@ -31,15 +31,19 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dar
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
+import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
+import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_unit.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/enums/exchange_rate_type_enum.dart';
+import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
@@ -55,8 +59,6 @@ import 'package:stackwallet/widgets/textfields/exchange_textfield.dart';
import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
-import '../../services/exchange/exchange_response.dart';
-
class ExchangeForm extends ConsumerStatefulWidget {
const ExchangeForm({
Key? key,
@@ -78,7 +80,7 @@ class _ExchangeFormState extends ConsumerState {
late final Coin? coin;
late final bool walletInitiated;
- final exchanges = [
+ var exchanges = [
MajesticBankExchange.instance,
ChangeNowExchange.instance,
TrocadorExchange.instance,
@@ -773,6 +775,14 @@ class _ExchangeFormState extends ConsumerState {
});
}
+ // Instantiate the Tor service.
+ torService = TorService.sharedInstance;
+
+ // Filter exchanges based on Tor support.
+ if (Prefs.instance.useTor) {
+ exchanges = Exchange.exchangesWithTorSupport;
+ }
+
super.initState();
}
@@ -1007,4 +1017,7 @@ class _ExchangeFormState extends ConsumerState {
],
);
}
+
+ // TorService instance.
+ late TorService torService;
}
diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart
index 25967801a..d491899f8 100644
--- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart
+++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart
@@ -252,10 +252,17 @@ class _Step4ViewState extends ConsumerState {
},
);
} else {
+ final memo =
+ manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
+ ? model.trade!.payInExtraId.isNotEmpty
+ ? model.trade!.payInExtraId
+ : null
+ : null;
txDataFuture = manager.prepareSend(
address: address,
amount: amount,
args: {
+ "memo": memo,
"feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider)
},
@@ -568,6 +575,74 @@ class _Step4ViewState extends ConsumerState {
const SizedBox(
height: 6,
),
+ if (model.trade!.payInExtraId.isNotEmpty)
+ RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Memo",
+ style:
+ STextStyles.itemSubtitle(context),
+ ),
+ GestureDetector(
+ onTap: () async {
+ final data = ClipboardData(
+ text:
+ model.trade!.payInExtraId);
+ await clipboard.setData(data);
+ if (mounted) {
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message:
+ "Copied to clipboard",
+ context: context,
+ ),
+ );
+ }
+ },
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ Assets.svg.copy,
+ color: Theme.of(context)
+ .extension()!
+ .infoItemIcons,
+ width: 10,
+ ),
+ const SizedBox(
+ width: 4,
+ ),
+ Text(
+ "Copy",
+ style:
+ STextStyles.link2(context),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 4,
+ ),
+ Text(
+ model.trade!.payInExtraId,
+ style:
+ STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ ),
+ if (model.trade!.payInExtraId.isNotEmpty)
+ const SizedBox(
+ height: 6,
+ ),
RoundedWhiteContainer(
child: Row(
children: [
diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart
index 4935a1b20..68b595900 100644
--- a/lib/pages/exchange_view/send_from_view.dart
+++ b/lib/pages/exchange_view/send_from_view.dart
@@ -268,10 +268,17 @@ class _SendFromCardState extends ConsumerState {
// if not firo then do normal send
if (shouldSendPublicFiroFunds == null) {
+ final memo =
+ manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
+ ? trade.payInExtraId.isNotEmpty
+ ? trade.payInExtraId
+ : null
+ : null;
txDataFuture = manager.prepareSend(
address: address,
amount: amount,
args: {
+ "memo": memo,
"feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider)
},
diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
index 13330e2f9..6f0108f66 100644
--- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
+++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
@@ -14,9 +14,11 @@ import 'package:stackwallet/models/exchange/aggregate_currency.dart';
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_option.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
+import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
@@ -44,6 +46,13 @@ class _ExchangeProviderOptionsState
required AggregateCurrency? sendCurrency,
required AggregateCurrency? receiveCurrency,
}) {
+ // If using Tor, only allow exchanges that support it.
+ if (Prefs.instance.useTor) {
+ if (!Exchange.exchangeNamesWithTorSupport.contains(exchangeName)) {
+ return false;
+ }
+ }
+
final send = sendCurrency?.forExchange(exchangeName);
if (send == null) return false;
diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart
index ff9b7ae24..24208a19c 100644
--- a/lib/pages/exchange_view/trade_details_view.dart
+++ b/lib/pages/exchange_view/trade_details_view.dart
@@ -850,6 +850,81 @@ class _TradeDetailsViewState extends ConsumerState {
: const SizedBox(
height: 12,
),
+ if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx)
+ RoundedWhiteContainer(
+ padding: isDesktop
+ ? const EdgeInsets.all(16)
+ : const EdgeInsets.all(12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Memo",
+ style: STextStyles.itemSubtitle(context),
+ ),
+ isDesktop
+ ? IconCopyButton(
+ data: trade.payInExtraId,
+ )
+ : GestureDetector(
+ onTap: () async {
+ final address = trade.payInExtraId;
+ await Clipboard.setData(
+ ClipboardData(
+ text: address,
+ ),
+ );
+ if (mounted) {
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message: "Copied to clipboard",
+ context: context,
+ ),
+ );
+ }
+ },
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ Assets.svg.copy,
+ width: 12,
+ height: 12,
+ color: Theme.of(context)
+ .extension()!
+ .infoItemIcons,
+ ),
+ const SizedBox(
+ width: 4,
+ ),
+ Text(
+ "Copy",
+ style: STextStyles.link2(context),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 4,
+ ),
+ SelectableText(
+ trade.payInExtraId,
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ ],
+ ),
+ ),
+ if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx)
+ isDesktop
+ ? const _Divider()
+ : const SizedBox(
+ height: 12,
+ ),
RoundedWhiteContainer(
padding: isDesktop
? const EdgeInsets.all(16)
diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart
index c3cad7a97..2303333e1 100644
--- a/lib/pages/home_view/home_view.dart
+++ b/lib/pages/home_view/home_view.dart
@@ -24,6 +24,7 @@ import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/providers/global/notifications_provider.dart';
import 'package:stackwallet/providers/ui/home_view_index_provider.dart';
import 'package:stackwallet/providers/ui/unread_notifications_provider.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/assets.dart';
@@ -32,6 +33,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
+import 'package:stackwallet/widgets/small_tor_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class HomeView extends ConsumerStatefulWidget {
@@ -55,6 +57,8 @@ class _HomeViewState extends ConsumerState {
bool _exitEnabled = false;
+ late TorConnectionStatus _currentSyncStatus;
+
// final _buyDataLoadingService = BuyDataLoadingService();
Future _onWillPop() async {
@@ -125,6 +129,20 @@ class _HomeViewState extends ConsumerState {
ref.read(notificationsProvider).startCheckingWatchedNotifications();
+ /// todo change to watch tor network
+ // if (ref.read(managerProvider).isRefreshing) {
+ // _currentSyncStatus = WalletSyncStatus.syncing;
+ // _currentNodeStatus = NodeConnectionStatus.connected;
+ // } else {
+ // _currentSyncStatus = WalletSyncStatus.synced;
+ // if (ref.read(managerProvider).isConnected) {
+ // _currentNodeStatus = NodeConnectionStatus.connected;
+ // } else {
+ // _currentNodeStatus = NodeConnectionStatus.disconnected;
+ // _currentSyncStatus = WalletSyncStatus.unableToSync;
+ // }
+ // }
+
super.initState();
}
@@ -200,6 +218,17 @@ class _HomeViewState extends ConsumerState {
],
),
actions: [
+ const Padding(
+ padding: EdgeInsets.only(
+ top: 10,
+ bottom: 10,
+ right: 10,
+ ),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: SmallTorIcon(),
+ ),
+ ),
Padding(
padding: const EdgeInsets.only(
top: 10,
diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart
index 041401767..17a518bf9 100644
--- a/lib/pages/ordinals/ordinal_details_view.dart
+++ b/lib/pages/ordinals/ordinal_details_view.dart
@@ -5,20 +5,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
-import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
+import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.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/prefs.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart';
@@ -230,11 +232,16 @@ class _OrdinalImageGroup extends StatelessWidget {
static const _spacing = 12.0;
Future _savePngToFile() async {
- final response = await get(Uri.parse(ordinal.content));
+ HTTP client = HTTP();
- if (response.statusCode != 200) {
- throw Exception(
- "statusCode=${response.statusCode} body=${response.bodyBytes}");
+ final response = await client.get(
+ url: Uri.parse(ordinal.content),
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+
+ if (response.code != 200) {
+ throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
}
final bytes = response.bodyBytes;
diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart
index d77e0e244..480813fc9 100644
--- a/lib/pages/receive_view/receive_view.dart
+++ b/lib/pages/receive_view/receive_view.dart
@@ -307,14 +307,20 @@ class _ReceiveViewState extends ConsumerState {
if (coin != Coin.epicCash &&
coin != Coin.ethereum &&
coin != Coin.banano &&
- coin != Coin.nano)
+ coin != Coin.nano &&
+ coin != Coin.stellar &&
+ coin != Coin.stellarTestnet &&
+ coin != Coin.tezos)
const SizedBox(
height: 12,
),
if (coin != Coin.epicCash &&
coin != Coin.ethereum &&
coin != Coin.banano &&
- coin != Coin.nano)
+ coin != Coin.nano &&
+ coin != Coin.stellar &&
+ coin != Coin.stellarTestnet &&
+ coin != Coin.tezos)
TextButton(
onPressed: generateNewAddress,
style: Theme.of(context)
diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart
index c3e66e758..01131b536 100644
--- a/lib/pages/send_view/send_view.dart
+++ b/lib/pages/send_view/send_view.dart
@@ -103,6 +103,7 @@ class _SendViewState extends ConsumerState {
late TextEditingController noteController;
late TextEditingController onChainNoteController;
late TextEditingController feeController;
+ late TextEditingController memoController;
late final SendViewAutoFillData? _data;
@@ -111,6 +112,9 @@ class _SendViewState extends ConsumerState {
final _onChainNoteFocusNode = FocusNode();
final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode();
+ final _memoFocus = FocusNode();
+
+ late final bool isStellar;
Amount? _amountToSend;
Amount? _cachedAmountToSend;
@@ -522,10 +526,15 @@ class _SendViewState extends ConsumerState {
},
);
} else {
+ final memo =
+ manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
+ ? memoController.text
+ : null;
txDataFuture = manager.prepareSend(
address: _address!,
amount: amount,
args: {
+ "memo": memo,
"feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null,
"UTXOs": (manager.hasCoinControlSupport &&
@@ -622,6 +631,7 @@ class _SendViewState extends ConsumerState {
walletId = widget.walletId;
clipboard = widget.clipboard;
scanner = widget.barcodeScanner;
+ isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
@@ -629,6 +639,7 @@ class _SendViewState extends ConsumerState {
noteController = TextEditingController();
onChainNoteController = TextEditingController();
feeController = TextEditingController();
+ memoController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged;
cryptoAmountController.addListener(onCryptoAmountChanged);
@@ -704,12 +715,14 @@ class _SendViewState extends ConsumerState {
noteController.dispose();
onChainNoteController.dispose();
feeController.dispose();
+ memoController.dispose();
_noteFocusNode.dispose();
_onChainNoteFocusNode.dispose();
_addressFocusNode.dispose();
_cryptoFocus.dispose();
_baseFocus.dispose();
+ _memoFocus.dispose();
super.dispose();
}
@@ -1298,6 +1311,88 @@ class _SendViewState extends ConsumerState {
),
),
),
+ const SizedBox(
+ height: 10,
+ ),
+ if (isStellar)
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ key: const Key("sendViewMemoFieldKey"),
+ controller: memoController,
+ readOnly: false,
+ autocorrect: false,
+ enableSuggestions: false,
+ focusNode: _memoFocus,
+ style: STextStyles.field(context),
+ onChanged: (_) {
+ setState(() {});
+ },
+ decoration: standardInputDecoration(
+ "Enter memo (optional)",
+ _memoFocus,
+ context,
+ ).copyWith(
+ contentPadding: const EdgeInsets.only(
+ left: 16,
+ top: 6,
+ bottom: 8,
+ right: 5,
+ ),
+ suffixIcon: Padding(
+ padding: memoController.text.isEmpty
+ ? const EdgeInsets.only(right: 8)
+ : const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceAround,
+ children: [
+ memoController.text.isNotEmpty
+ ? TextFieldIconButton(
+ semanticsLabel:
+ "Clear Button. Clears The Memo Field Input.",
+ key: const Key(
+ "sendViewClearMemoFieldButtonKey"),
+ onTap: () {
+ memoController.text = "";
+ setState(() {});
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ semanticsLabel:
+ "Paste Button. Pastes From Clipboard To Memo Field Input.",
+ key: const Key(
+ "sendViewPasteMemoFieldButtonKey"),
+ onTap: () async {
+ final ClipboardData? data =
+ await clipboard.getData(
+ Clipboard
+ .kTextPlain);
+ if (data?.text != null &&
+ data!
+ .text!.isNotEmpty) {
+ String content =
+ data.text!.trim();
+
+ memoController.text =
+ content.trim();
+
+ setState(() {});
+ }
+ },
+ child: const ClipboardIcon(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
Builder(
builder: (_) {
final error = _updateInvalidAddressText(
@@ -1817,7 +1912,8 @@ class _SendViewState extends ConsumerState {
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
- enableSuggestions: Util.isDesktop ? false : true,
+ enableSuggestions:
+ Util.isDesktop ? false : true,
maxLength: 256,
controller: onChainNoteController,
focusNode: _onChainNoteFocusNode,
@@ -1828,25 +1924,27 @@ class _SendViewState extends ConsumerState {
_onChainNoteFocusNode,
context,
).copyWith(
- suffixIcon: onChainNoteController.text.isNotEmpty
+ suffixIcon: onChainNoteController
+ .text.isNotEmpty
? Padding(
- padding:
- const EdgeInsets.only(right: 0),
- child: UnconstrainedBox(
- child: Row(
- children: [
- TextFieldIconButton(
- child: const XIcon(),
- onTap: () async {
- setState(() {
- onChainNoteController.text = "";
- });
- },
+ padding:
+ const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(),
+ onTap: () async {
+ setState(() {
+ onChainNoteController
+ .text = "";
+ });
+ },
+ ),
+ ],
+ ),
),
- ],
- ),
- ),
- )
+ )
: null,
),
),
@@ -1856,8 +1954,9 @@ class _SendViewState extends ConsumerState {
height: 12,
),
Text(
- (coin == Coin.epicCash) ? "Local Note (optional)"
- : "Note (optional)",
+ (coin == Coin.epicCash)
+ ? "Local Note (optional)"
+ : "Note (optional)",
style: STextStyles.smallMed12(context),
textAlign: TextAlign.left,
),
diff --git a/lib/pages/settings_views/global_settings_view/about_view.dart b/lib/pages/settings_views/global_settings_view/about_view.dart
index 5daa1e81a..0d3e8d811 100644
--- a/lib/pages/settings_views/global_settings_view/about_view.dart
+++ b/lib/pages/settings_views/global_settings_view/about_view.dart
@@ -15,11 +15,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:http/http.dart';
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:package_info_plus/package_info_plus.dart';
+import 'package:stackwallet/networking/http.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/prefs.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';
@@ -39,14 +41,17 @@ Future doesCommitExist(
String commit,
) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
- final Client client = Client();
+ // final Client client = Client();
+ HTTP client = HTTP();
try {
final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
final commitQuery = await client.get(
- uri,
+ url: uri,
headers: {'Content-Type': 'application/json'},
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final response = jsonDecode(commitQuery.body.toString());
@@ -76,14 +81,16 @@ Future isHeadCommit(
String commit,
) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
- final Client client = Client();
+ HTTP client = HTTP();
try {
final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
final commitQuery = await client.get(
- uri,
+ url: uri,
headers: {'Content-Type': 'application/json'},
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final response = jsonDecode(commitQuery.body.toString());
diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart
index 7cadc1a77..29984b928 100644
--- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart
+++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart
@@ -25,6 +25,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back
import 'package:stackwallet/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
+import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
@@ -159,6 +160,18 @@ class GlobalSettingsView extends StatelessWidget {
const SizedBox(
height: 8,
),
+ SettingsListButton(
+ iconAssetName: Assets.svg.tor,
+ iconSize: 18,
+ title: "Tor Settings",
+ onPressed: () {
+ Navigator.of(context)
+ .pushNamed(TorSettingsView.routeName);
+ },
+ ),
+ const SizedBox(
+ height: 8,
+ ),
SettingsListButton(
iconAssetName: Assets.svg.node,
iconSize: 16,
diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart
index 4e42e3733..52a523f22 100644
--- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart
+++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart
@@ -167,77 +167,77 @@ class HiddenSettings extends StatelessWidget {
// ),
// );
// }),
- const SizedBox(
- height: 12,
- ),
- Consumer(builder: (_, ref, __) {
- return GestureDetector(
- onTap: () async {
- ref
- .read(priceAnd24hChangeNotifierProvider)
- .tokenContractAddressesToCheck
- .add(
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
- ref
- .read(priceAnd24hChangeNotifierProvider)
- .tokenContractAddressesToCheck
- .add(
- "0xdAC17F958D2ee523a2206206994597C13D831ec7");
- await ref
- .read(priceAnd24hChangeNotifierProvider)
- .updatePrice();
-
- final x = ref
- .read(priceAnd24hChangeNotifierProvider)
- .getTokenPrice(
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
-
- print(
- "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x");
- },
- child: RoundedWhiteContainer(
- child: Text(
- "Click me",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(context)
- .extension()!
- .accentColorDark),
- ),
- ),
- );
- }),
- const SizedBox(
- height: 12,
- ),
- Consumer(builder: (_, ref, __) {
- return GestureDetector(
- onTap: () async {
- // final erc20 = Erc20ContractInfo(
- // contractAddress: 'some con',
- // name: "loonamsn",
- // symbol: "DD",
- // decimals: 19,
- // );
- //
- // final json = erc20.toJson();
- //
- // print(json);
- //
- // final ee = EthContractInfo.fromJson(json);
- //
- // print(ee);
- },
- child: RoundedWhiteContainer(
- child: Text(
- "Click me",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(context)
- .extension()!
- .accentColorDark),
- ),
- ),
- );
- }),
+ // const SizedBox(
+ // height: 12,
+ // ),
+ // Consumer(builder: (_, ref, __) {
+ // return GestureDetector(
+ // onTap: () async {
+ // ref
+ // .read(priceAnd24hChangeNotifierProvider)
+ // .tokenContractAddressesToCheck
+ // .add(
+ // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
+ // ref
+ // .read(priceAnd24hChangeNotifierProvider)
+ // .tokenContractAddressesToCheck
+ // .add(
+ // "0xdAC17F958D2ee523a2206206994597C13D831ec7");
+ // await ref
+ // .read(priceAnd24hChangeNotifierProvider)
+ // .updatePrice();
+ //
+ // final x = ref
+ // .read(priceAnd24hChangeNotifierProvider)
+ // .getTokenPrice(
+ // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
+ //
+ // print(
+ // "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x");
+ // },
+ // child: RoundedWhiteContainer(
+ // child: Text(
+ // "Click me",
+ // style: STextStyles.button(context).copyWith(
+ // color: Theme.of(context)
+ // .extension()!
+ // .accentColorDark),
+ // ),
+ // ),
+ // );
+ // }),
+ // const SizedBox(
+ // height: 12,
+ // ),
+ // Consumer(builder: (_, ref, __) {
+ // return GestureDetector(
+ // onTap: () async {
+ // // final erc20 = Erc20ContractInfo(
+ // // contractAddress: 'some con',
+ // // name: "loonamsn",
+ // // symbol: "DD",
+ // // decimals: 19,
+ // // );
+ // //
+ // // final json = erc20.toJson();
+ // //
+ // // print(json);
+ // //
+ // // final ee = EthContractInfo.fromJson(json);
+ // //
+ // // print(ee);
+ // },
+ // child: RoundedWhiteContainer(
+ // child: Text(
+ // "Click me",
+ // style: STextStyles.button(context).copyWith(
+ // color: Theme.of(context)
+ // .extension()!
+ // .accentColorDark),
+ // ),
+ // ),
+ // );
+ // }),
const SizedBox(
height: 12,
),
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
index 56b008093..2bb833fd1 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
@@ -27,6 +27,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
+import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
@@ -193,13 +194,20 @@ class _AddEditNodeViewState extends ConsumerState {
try {
// await client.getSyncStatus();
} catch (_) {}
+ break;
+ case Coin.stellar:
+ case Coin.stellarTestnet:
+ try {
+ testPassed =
+ await testStellarNodeConnection(formData.host!, formData.port!);
+ } catch (_) {}
+ break;
case Coin.nano:
case Coin.banano:
- case Coin.stellar:
- case Coin.stellarTestnet:
+ case Coin.tezos:
throw UnimplementedError();
- //TODO: check network/node
+ //TODO: check network/node
}
if (showFlushBar && mounted) {
@@ -730,6 +738,7 @@ class _NodeFormState extends ConsumerState {
case Coin.namecoin:
case Coin.bitcoincash:
case Coin.particl:
+ case Coin.tezos:
case Coin.bitcoinTestNet:
case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet:
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
index 59ff1efad..c05cbbca5 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
@@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_eth_node_connection.dart';
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
+import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
@@ -172,10 +173,17 @@ class _NodeDetailsViewState extends ConsumerState {
case Coin.nano:
case Coin.banano:
+ case Coin.tezos:
+ throw UnimplementedError();
+ //TODO: check network/node
case Coin.stellar:
case Coin.stellarTestnet:
- throw UnimplementedError();
- //TODO: check network/node
+ try {
+ testPassed = await testStellarNodeConnection(node!.host, node.port);
+ } catch (_) {
+ testPassed = false;
+ }
+ break;
}
if (testPassed) {
diff --git a/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart
new file mode 100644
index 000000000..097a68b93
--- /dev/null
+++ b/lib/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart
@@ -0,0 +1,499 @@
+/*
+ * This file is part of Stack Wallet.
+ *
+ * Copyright (c) 2023 Cypher Stack
+ * All Rights Reserved.
+ * The code is distributed under GPLv3 license, see LICENSE file for details.
+ * Generated by Cypher Stack on 2023-05-26
+ *
+ */
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/providers/global/prefs_provider.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
+import 'package:stackwallet/services/tor_service.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/widgets/background.dart';
+import 'package:stackwallet/widgets/conditional_parent.dart';
+import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
+import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
+import 'package:stackwallet/widgets/desktop/secondary_button.dart';
+import 'package:stackwallet/widgets/rounded_white_container.dart';
+import 'package:stackwallet/widgets/stack_dialog.dart';
+import 'package:stackwallet/widgets/tor_subscription.dart';
+
+class TorSettingsView extends ConsumerStatefulWidget {
+ const TorSettingsView({Key? key}) : super(key: key);
+
+ static const String routeName = "/torSettings";
+
+ @override
+ ConsumerState createState() => _TorSettingsViewState();
+}
+
+class _TorSettingsViewState extends ConsumerState {
+ @override
+ Widget build(BuildContext context) {
+ return Background(
+ child: Scaffold(
+ backgroundColor: Colors.transparent,
+ appBar: AppBar(
+ automaticallyImplyLeading: false,
+ backgroundColor:
+ Theme.of(context).extension()!.backgroundAppBar,
+ leading: AppBarBackButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ title: Text(
+ "Tor settings",
+ style: STextStyles.navBarTitle(context),
+ ),
+ actions: [
+ AspectRatio(
+ aspectRatio: 1,
+ child: AppBarIconButton(
+ icon: SvgPicture.asset(
+ Assets.svg.circleQuestion,
+ ),
+ onPressed: () {
+ showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return const StackDialog(
+ title: "What is Tor?",
+ message:
+ "Short for \"The Onion Router\", is an open-source software that enables internet communication"
+ " to remain anonymous by routing internet traffic through a series of layered nodes,"
+ " to obscure the origin and destination of data.",
+ rightButton: SecondaryButton(
+ label: "Close",
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsets.all(10.0),
+ child: TorIcon(),
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 30,
+ ),
+ const TorButton(),
+ const SizedBox(
+ height: 8,
+ ),
+ RoundedWhiteContainer(
+ child: Consumer(
+ builder: (_, ref, __) {
+ return RawMaterialButton(
+ // splashColor: Theme.of(context).extension()!.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: [
+ Row(
+ children: [
+ Text(
+ "Tor killswitch",
+ style: STextStyles.titleBold12(context),
+ ),
+ const SizedBox(width: 8),
+ GestureDetector(
+ onTap: () {
+ showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return const StackDialog(
+ title: "What is Tor killswitch?",
+ message:
+ "A security feature that protects your information from accidental exposure by"
+ " disconnecting your device from the Tor network if the"
+ " connection is disrupted or compromised.",
+ rightButton: SecondaryButton(
+ label: "Close",
+ ),
+ );
+ },
+ );
+ },
+ child: SvgPicture.asset(
+ Assets.svg.circleInfo,
+ height: 16,
+ width: 16,
+ color: Theme.of(context)
+ .extension()!
+ .infoItemLabel,
+ ),
+ ),
+ ],
+ ),
+ SizedBox(
+ height: 20,
+ width: 40,
+ child: DraggableSwitchButton(
+ isOn: ref.watch(
+ prefsChangeNotifierProvider
+ .select((value) => value.torKillSwitch),
+ ),
+ onValueChanged: (newValue) {
+ ref
+ .read(prefsChangeNotifierProvider)
+ .torKillSwitch = newValue;
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class TorIcon extends ConsumerStatefulWidget {
+ const TorIcon({super.key});
+
+ @override
+ ConsumerState createState() => _TorIconState();
+}
+
+class _TorIconState extends ConsumerState {
+ late TorConnectionStatus _status;
+
+ Color _color(
+ TorConnectionStatus status,
+ StackColors colors,
+ ) {
+ switch (status) {
+ case TorConnectionStatus.disconnected:
+ return colors.textSubtitle3;
+
+ case TorConnectionStatus.connected:
+ return colors.accentColorGreen;
+
+ case TorConnectionStatus.connecting:
+ return colors.accentColorYellow;
+ }
+ }
+
+ String _label(
+ TorConnectionStatus status,
+ StackColors colors,
+ ) {
+ switch (status) {
+ case TorConnectionStatus.disconnected:
+ return "CONNECT";
+
+ case TorConnectionStatus.connected:
+ return "STOP";
+
+ case TorConnectionStatus.connecting:
+ return "CONNECTING";
+ }
+ }
+
+ bool _tapLock = false;
+
+ Future onTap() async {
+ if (_tapLock) {
+ return;
+ }
+ _tapLock = true;
+ try {
+ // Connect or disconnect when the user taps the status.
+ switch (_status) {
+ case TorConnectionStatus.disconnected:
+ await _connectTor(ref, context);
+ break;
+
+ case TorConnectionStatus.connected:
+ await _disconnectTor(ref, context);
+
+ break;
+
+ case TorConnectionStatus.connecting:
+ // Do nothing.
+ break;
+ }
+ } catch (_) {
+ // any exceptions should already be handled with error dialogs
+ // this try catch is just extra protection to ensure _tapLock gets reset
+ // in the finally block in the event of an unknown error
+ } finally {
+ _tapLock = false;
+ }
+ }
+
+ @override
+ void initState() {
+ _status = ref.read(pTorService).enabled
+ ? TorConnectionStatus.connected
+ : TorConnectionStatus.disconnected;
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TorSubscription(
+ onTorStatusChanged: (status) {
+ setState(() {
+ _status = status;
+ });
+ },
+ child: ConditionalParent(
+ condition: _status != TorConnectionStatus.connecting,
+ builder: (child) => GestureDetector(
+ onTap: onTap,
+ child: child,
+ ),
+ child: SizedBox(
+ width: 220,
+ height: 220,
+ child: Stack(
+ alignment: AlignmentDirectional.center,
+ children: [
+ SvgPicture.asset(
+ Assets.svg.tor,
+ color: _color(
+ _status,
+ Theme.of(context).extension()!,
+ ),
+ width: 200,
+ height: 200,
+ ),
+ Text(
+ _label(
+ _status,
+ Theme.of(context).extension()!,
+ ),
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(context).extension()!.popupBG,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class TorButton extends ConsumerStatefulWidget {
+ const TorButton({super.key});
+
+ @override
+ ConsumerState createState() => _TorButtonState();
+}
+
+class _TorButtonState extends ConsumerState {
+ late TorConnectionStatus _status;
+
+ Color _color(
+ TorConnectionStatus status,
+ StackColors colors,
+ ) {
+ switch (status) {
+ case TorConnectionStatus.disconnected:
+ return colors.textSubtitle3;
+
+ case TorConnectionStatus.connected:
+ return colors.accentColorGreen;
+
+ case TorConnectionStatus.connecting:
+ return colors.accentColorYellow;
+ }
+ }
+
+ String _label(
+ TorConnectionStatus status,
+ StackColors colors,
+ ) {
+ switch (status) {
+ case TorConnectionStatus.disconnected:
+ return "Disconnected";
+
+ case TorConnectionStatus.connected:
+ return "Connected";
+
+ case TorConnectionStatus.connecting:
+ return "Connecting";
+ }
+ }
+
+ bool _tapLock = false;
+
+ Future onTap() async {
+ if (_tapLock) {
+ return;
+ }
+ _tapLock = true;
+ try {
+ // Connect or disconnect when the user taps the status.
+ switch (_status) {
+ case TorConnectionStatus.disconnected:
+ await _connectTor(ref, context);
+ break;
+
+ case TorConnectionStatus.connected:
+ await _disconnectTor(ref, context);
+
+ break;
+
+ case TorConnectionStatus.connecting:
+ // Do nothing.
+ break;
+ }
+ } catch (_) {
+ // any exceptions should already be handled with error dialogs
+ // this try catch is just extra protection to ensure _tapLock gets reset
+ // in the finally block in the event of an unknown error
+ } finally {
+ _tapLock = false;
+ }
+ }
+
+ @override
+ void initState() {
+ _status = ref.read(pTorService).enabled
+ ? TorConnectionStatus.connected
+ : TorConnectionStatus.disconnected;
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TorSubscription(
+ onTorStatusChanged: (status) {
+ setState(() {
+ _status = status;
+ });
+ },
+ child: GestureDetector(
+ onTap: onTap,
+ child: RoundedWhiteContainer(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Row(
+ children: [
+ Text(
+ "Tor status",
+ style: STextStyles.titleBold12(context),
+ ),
+ const Spacer(),
+ Text(
+ _label(
+ _status,
+ Theme.of(context).extension()!,
+ ),
+ style: STextStyles.itemSubtitle(context).copyWith(
+ color: _color(
+ _status,
+ Theme.of(context).extension()!,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Connect to the Tor network.
+///
+/// This method is called when the user taps the "Connect" button.
+///
+/// Throws an exception if the Tor service fails to start.
+///
+/// Returns a Future that completes when the Tor service has started.
+Future _connectTor(WidgetRef ref, BuildContext context) async {
+ // Init the Tor service if it hasn't already been.
+ ref.read(pTorService).init();
+
+ // Start the Tor service.
+ try {
+ await ref.read(pTorService).start();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = true;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error starting tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ // TODO: show dialog with error message
+ }
+
+ return;
+}
+
+/// Disconnect from the Tor network.
+///
+/// This method is called when the user taps the "Disconnect" button.
+///
+/// Throws an exception if the Tor service fails to stop.
+///
+/// Returns a Future that completes when the Tor service has stopped.
+Future _disconnectTor(WidgetRef ref, BuildContext context) async {
+ // Stop the Tor service.
+ try {
+ await ref.read(pTorService).stop();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = false;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error stopping tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ // TODO: show dialog with error message
+ }
+}
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
index b447a53ee..48f805691 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
@@ -27,12 +27,15 @@ import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
+import 'package:stackwallet/services/tor_service.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/logger.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart';
@@ -92,6 +95,13 @@ class _WalletNetworkSettingsViewState
late int _blocksRemaining;
bool _advancedIsExpanded = false;
+ /// The current status of the Tor connection.
+ late TorConnectionStatus _torConnectionStatus;
+
+ /// The subscription to the TorConnectionStatusChangedEvent.
+ late final StreamSubscription
+ _torConnectionStatusSubscription;
+
Future _attemptRescan() async {
if (!Platform.isLinux) await Wakelock.enable();
@@ -268,6 +278,25 @@ class _WalletNetworkSettingsViewState
// }
// },
// );
+
+ // Initialize the TorConnectionStatus.
+ _torConnectionStatus = ref.read(pTorService).enabled
+ ? TorConnectionStatus.connected
+ : TorConnectionStatus.disconnected;
+
+ // Subscribe to the TorConnectionStatusChangedEvent.
+ _torConnectionStatusSubscription =
+ eventBus.on().listen(
+ (event) async {
+ // Rebuild the widget.
+ setState(() {
+ _torConnectionStatus = event.newStatus;
+ });
+
+ // TODO implement spinner or animations and control from here
+ },
+ );
+
super.initState();
}
@@ -277,6 +306,7 @@ class _WalletNetworkSettingsViewState
_syncStatusSubscription.cancel();
_refreshSubscription.cancel();
_blocksRemainingSubscription?.cancel();
+ _torConnectionStatusSubscription.cancel();
super.dispose();
}
@@ -340,92 +370,98 @@ class _WalletNetworkSettingsViewState
style: STextStyles.navBarTitle(context),
),
actions: [
- Padding(
- padding: const EdgeInsets.only(
- top: 10,
- bottom: 10,
- right: 10,
- ),
- child: AspectRatio(
- aspectRatio: 1,
- child: AppBarIconButton(
- key: const Key(
- "walletNetworkSettingsAddNewNodeViewButton"),
- size: 36,
- shadows: const [],
- color: Theme.of(context)
- .extension()!
- .background,
- icon: SvgPicture.asset(
- Assets.svg.verticalEllipsis,
+ if (ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(widget.walletId)
+ .coin !=
+ Coin.epicCash)
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 10,
+ bottom: 10,
+ right: 10,
+ ),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: AppBarIconButton(
+ key: const Key(
+ "walletNetworkSettingsAddNewNodeViewButton"),
+ size: 36,
+ shadows: const [],
color: Theme.of(context)
.extension()!
- .accentColorDark,
- width: 20,
- height: 20,
- ),
- onPressed: () {
- showDialog(
- barrierColor: Colors.transparent,
- barrierDismissible: true,
- context: context,
- builder: (_) {
- return Stack(
- children: [
- Positioned(
- top: 9,
- right: 10,
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context)
- .extension()!
- .popupBG,
- borderRadius: BorderRadius.circular(
- Constants.size.circularBorderRadius),
- // boxShadow: [CFColors.standardBoxShadow],
- boxShadow: const [],
- ),
- child: Column(
- crossAxisAlignment:
- CrossAxisAlignment.start,
- children: [
- GestureDetector(
- onTap: () {
- Navigator.of(context).pop();
- showDialog(
- context: context,
- useSafeArea: false,
- barrierDismissible: true,
- builder: (context) {
- return ConfirmFullRescanDialog(
- onConfirm: _attemptRescan,
- );
- },
- );
- },
- child: RoundedWhiteContainer(
- child: Material(
- color: Colors.transparent,
- child: Text(
- "Rescan blockchain",
- style:
- STextStyles.baseXS(context),
+ .background,
+ icon: SvgPicture.asset(
+ Assets.svg.verticalEllipsis,
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark,
+ width: 20,
+ height: 20,
+ ),
+ onPressed: () {
+ showDialog(
+ barrierColor: Colors.transparent,
+ barrierDismissible: true,
+ context: context,
+ builder: (_) {
+ return Stack(
+ children: [
+ Positioned(
+ top: 9,
+ right: 10,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .extension()!
+ .popupBG,
+ borderRadius: BorderRadius.circular(
+ Constants
+ .size.circularBorderRadius),
+ // boxShadow: [CFColors.standardBoxShadow],
+ boxShadow: const [],
+ ),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ GestureDetector(
+ onTap: () {
+ Navigator.of(context).pop();
+ showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return ConfirmFullRescanDialog(
+ onConfirm: _attemptRescan,
+ );
+ },
+ );
+ },
+ child: RoundedWhiteContainer(
+ child: Material(
+ color: Colors.transparent,
+ child: Text(
+ "Rescan blockchain",
+ style: STextStyles.baseXS(
+ context),
+ ),
),
),
),
- ),
- ],
+ ],
+ ),
),
),
- ),
- ],
- );
- },
- );
- },
+ ],
+ );
+ },
+ );
+ },
+ ),
),
),
- ),
],
),
body: Padding(
@@ -521,14 +557,6 @@ class _WalletNetworkSettingsViewState
"Synchronized",
style: STextStyles.w600_12(context),
),
- Text(
- "100%",
- style: STextStyles.syncPercent(context).copyWith(
- color: Theme.of(context)
- .extension()!
- .accentColorGreen,
- ),
- ),
],
),
),
@@ -749,6 +777,161 @@ class _WalletNetworkSettingsViewState
SizedBox(
height: isDesktop ? 32 : 20,
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Tor status",
+ textAlign: TextAlign.left,
+ style: isDesktop
+ ? STextStyles.desktopTextExtraExtraSmall(context)
+ : STextStyles.smallMed12(context),
+ ),
+ if (ref.watch(
+ prefsChangeNotifierProvider.select((value) => value.useTor)))
+ GestureDetector(
+ onTap: () async {
+ // Stop the Tor service.
+ try {
+ await ref.read(pTorService).stop();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = false;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error stopping tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ }
+ },
+ child: Text(
+ "Disconnect",
+ style: STextStyles.link2(context),
+ ),
+ ),
+ if (!ref.watch(
+ prefsChangeNotifierProvider.select((value) => value.useTor)))
+ GestureDetector(
+ onTap: () async {
+ // Init the Tor service if it hasn't already been.
+ ref.read(pTorService).init();
+
+ // Start the Tor service.
+ try {
+ await ref.read(pTorService).start();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = true;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error starting tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ }
+ },
+ child: Text(
+ "Connect",
+ style: STextStyles.link2(context),
+ ),
+ ),
+ ],
+ ),
+ SizedBox(
+ height: isDesktop ? 12 : 9,
+ ),
+ RoundedWhiteContainer(
+ borderColor: isDesktop
+ ? Theme.of(context).extension()!.background
+ : null,
+ padding:
+ isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12),
+ child: Row(
+ children: [
+ if (ref.watch(prefsChangeNotifierProvider
+ .select((value) => value.useTor)))
+ Container(
+ width: _iconSize,
+ height: _iconSize,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorGreen
+ .withOpacity(0.2),
+ borderRadius: BorderRadius.circular(_iconSize),
+ ),
+ child: Center(
+ child: SvgPicture.asset(
+ Assets.svg.tor,
+ height: isDesktop ? 19 : 14,
+ width: isDesktop ? 19 : 14,
+ color: Theme.of(context)
+ .extension()!
+ .accentColorGreen,
+ ),
+ ),
+ ),
+ if (!ref.watch(prefsChangeNotifierProvider
+ .select((value) => value.useTor)))
+ Container(
+ width: _iconSize,
+ height: _iconSize,
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.08),
+ borderRadius: BorderRadius.circular(_iconSize),
+ ),
+ child: Center(
+ child: SvgPicture.asset(
+ Assets.svg.tor,
+ height: isDesktop ? 19 : 14,
+ width: isDesktop ? 19 : 14,
+ color: Theme.of(context)
+ .extension()!
+ .textDark,
+ ),
+ ),
+ ),
+ SizedBox(
+ width: _boxPadding,
+ ),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Tor status",
+ style: STextStyles.desktopTextExtraExtraSmall(context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .textDark,
+ ),
+ ),
+ if (_torConnectionStatus == TorConnectionStatus.connected)
+ Text(
+ "Connected",
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ ),
+ if (_torConnectionStatus == TorConnectionStatus.connecting)
+ Text(
+ "Connecting...",
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ ),
+ if (_torConnectionStatus ==
+ TorConnectionStatus.disconnected)
+ Text(
+ "Disconnected",
+ style: STextStyles.desktopTextExtraExtraSmall(context),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ SizedBox(
+ height: isDesktop ? 32 : 20,
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -786,11 +969,21 @@ class _WalletNetworkSettingsViewState
.select((value) => value.getManager(widget.walletId).coin)),
popBackToRoute: WalletNetworkSettingsView.routeName,
),
- if (isDesktop)
+ if (isDesktop &&
+ ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(widget.walletId)
+ .coin !=
+ Coin.epicCash)
const SizedBox(
height: 32,
),
- if (isDesktop)
+ if (isDesktop &&
+ ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(widget.walletId)
+ .coin !=
+ Coin.epicCash)
Padding(
padding: const EdgeInsets.only(
bottom: 12,
@@ -806,7 +999,12 @@ class _WalletNetworkSettingsViewState
],
),
),
- if (isDesktop)
+ if (isDesktop &&
+ ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(widget.walletId)
+ .coin !=
+ Coin.epicCash)
RoundedWhiteContainer(
borderColor: isDesktop
? Theme.of(context).extension()!.background
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
index 6b7e57bef..fcacc60d4 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart
@@ -13,7 +13,6 @@ import 'dart:async';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/models/epicbox_config_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
@@ -37,14 +36,11 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
-import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
-import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
-import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart';
/// [eventBus] should only be set during testing
@@ -310,61 +306,6 @@ class _WalletSettingsViewState extends ConsumerState {
);
},
),
- if (coin == Coin.firo)
- const SizedBox(
- height: 8,
- ),
- if (coin == Coin.firo)
- Consumer(
- builder: (_, ref, __) {
- return SettingsListButton(
- iconAssetName: Assets.svg.eye,
- title: "Clear electrumx cache",
- onPressed: () async {
- String? result;
- await showDialog(
- useSafeArea: false,
- barrierDismissible: true,
- context: context,
- builder: (_) => StackOkDialog(
- title:
- "Are you sure you want to clear "
- "${coin.prettyName} electrumx cache?",
- onOkPressed: (value) {
- result = value;
- },
- leftButton: SecondaryButton(
- label: "Cancel",
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- ),
- );
-
- if (result == "OK" && mounted) {
- await showLoading(
- whileFuture: Future.wait(
- [
- Future.delayed(
- const Duration(
- milliseconds: 1500,
- ),
- ),
- DB.instance
- .clearSharedTransactionCache(
- coin: coin,
- ),
- ],
- ),
- context: context,
- message: "Clearing cache...",
- );
- }
- },
- );
- },
- ),
if (coin == Coin.nano || coin == Coin.banano)
const SizedBox(
height: 8,
diff --git a/lib/pages/wallet_view/sub_widgets/tx_icon.dart b/lib/pages/wallet_view/sub_widgets/tx_icon.dart
index 46972c49b..d86ad6e8d 100644
--- a/lib/pages/wallet_view/sub_widgets/tx_icon.dart
+++ b/lib/pages/wallet_view/sub_widgets/tx_icon.dart
@@ -35,7 +35,6 @@ class TxIcon extends ConsumerWidget {
String _getAssetName(
bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) {
-
if (!isReceived && transaction.subType == TransactionSubType.mint) {
if (isCancelled) {
return Assets.svg.anonymizeFailed;
@@ -48,7 +47,7 @@ class TxIcon extends ConsumerWidget {
if (isReceived) {
if (isCancelled) {
- return assets.receiveCancelled;
+ return assets.receive;
}
if (isPending) {
return assets.receivePending;
diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
index 88d0d8b1a..f358aef5e 100644
--- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
+++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
@@ -358,6 +358,8 @@ class _TransactionDetailsViewState
final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight));
+ print("THIS TRANSACTION IS $_transaction");
+
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
@@ -1577,11 +1579,7 @@ class _TransactionDetailsViewState
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: (coin == Coin.epicCash &&
- _transaction.isConfirmed(
- currentHeight,
- coin.requiredConfirmations,
- ) ==
- false &&
+ _transaction.getConfirmations(currentHeight) < 1 &&
_transaction.isCancelled == false)
? ConditionalParent(
condition: isDesktop,
diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart
index 7c6ca034b..e38a38cdd 100644
--- a/lib/pages/wallet_view/wallet_view.dart
+++ b/lib/pages/wallet_view/wallet_view.dart
@@ -72,6 +72,7 @@ import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
+import 'package:stackwallet/widgets/small_tor_icon.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
@@ -555,6 +556,17 @@ class _WalletViewState extends ConsumerState {
],
),
actions: [
+ const Padding(
+ padding: EdgeInsets.only(
+ top: 10,
+ bottom: 10,
+ right: 10,
+ ),
+ child: AspectRatio(
+ aspectRatio: 1,
+ child: SmallTorIcon(),
+ ),
+ ),
Padding(
padding: const EdgeInsets.only(
top: 10,
diff --git a/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart b/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart
index e56d2644c..8827641fa 100644
--- a/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart
+++ b/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart
@@ -9,76 +9,137 @@
*/
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/buy_view/buy_form.dart';
+import 'package:stackwallet/providers/global/prefs_provider.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
+import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
-class DesktopBuyView extends StatefulWidget {
+class DesktopBuyView extends ConsumerStatefulWidget {
const DesktopBuyView({Key? key}) : super(key: key);
static const String routeName = "/desktopBuyView";
@override
- State createState() => _DesktopBuyViewState();
+ ConsumerState createState() => _DesktopBuyViewState();
}
-class _DesktopBuyViewState extends State {
+class _DesktopBuyViewState extends ConsumerState {
+ late bool torEnabled = false;
+
+ @override
+ void initState() {
+ WidgetsBinding.instance.addPostFrameCallback((_) async {
+ setState(() {
+ torEnabled = ref.read(prefsChangeNotifierProvider).useTor;
+ });
+ });
+ super.initState();
+ }
+
@override
Widget build(BuildContext context) {
- return DesktopScaffold(
- appBar: DesktopAppBar(
- isCompactHeight: true,
- leading: Padding(
- padding: const EdgeInsets.only(
- left: 24,
- ),
- child: Text(
- "Buy crypto",
- style: STextStyles.desktopH3(context),
- ),
- ),
- ),
- body: Padding(
- padding: const EdgeInsets.only(
- left: 24,
- right: 24,
- bottom: 24,
- ),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: const [
- SizedBox(
- height: 16,
- ),
- RoundedWhiteContainer(
- padding: EdgeInsets.all(24),
- child: BuyForm(),
- ),
- ],
+ return Stack(
+ children: [
+ DesktopScaffold(
+ appBar: DesktopAppBar(
+ isCompactHeight: true,
+ leading: Padding(
+ padding: const EdgeInsets.only(
+ left: 24,
+ ),
+ child: Text(
+ "Buy crypto",
+ style: STextStyles.desktopH3(context),
),
),
- const SizedBox(
- width: 16,
+ ),
+ body: Padding(
+ padding: const EdgeInsets.only(
+ left: 24,
+ right: 24,
+ bottom: 24,
),
- // Expanded(
- // child: Row(
- // children: const [
- // Expanded(
- // child: DesktopTradeHistory(),
- // ),
- // ],
- // ),
- // ),
- ],
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: const [
+ SizedBox(
+ height: 16,
+ ),
+ RoundedWhiteContainer(
+ padding: EdgeInsets.all(24),
+ child: BuyForm(),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ width: 16,
+ ),
+ // Expanded(
+ // child: Row(
+ // children: const [
+ // Expanded(
+ // child: DesktopTradeHistory(),
+ // ),
+ // ],
+ // ),
+ // ),
+ ],
+ ),
+ ),
),
- ),
+ if (torEnabled)
+ Container(
+ color: Theme.of(context)
+ .extension()!
+ .overlay
+ .withOpacity(0.7),
+ height: MediaQuery.of(context).size.height,
+ width: MediaQuery.of(context).size.width,
+ child: DesktopDialog(
+ maxHeight: 200,
+ maxWidth: 350,
+ child: Padding(
+ padding: const EdgeInsets.all(
+ 15.0,
+ ),
+ child: Column(
+ // crossAxisAlignment: CrossAxisAlignment.center,
+ // mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Tor is enabled",
+ textAlign: TextAlign.center,
+ style: STextStyles.pageTitleH1(context),
+ ),
+ const SizedBox(
+ height: 30,
+ ),
+ Text(
+ "Purchasing not available while Tor is enabled",
+ textAlign: TextAlign.center,
+ style: STextStyles.desktopTextMedium(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .infoItemLabel,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
);
}
}
diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart
index 8b30b2e2a..437e45210 100644
--- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart
+++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_4.dart
@@ -155,6 +155,23 @@ class _DesktopStep4State extends ConsumerState {
height: 1,
color: Theme.of(context).extension()!.background,
),
+ if (ref.watch(desktopExchangeModelProvider
+ .select((value) => value!.trade?.payInExtraId)) !=
+ null)
+ DesktopStepItem(
+ vertical: true,
+ label: "Memo",
+ value: ref.watch(desktopExchangeModelProvider
+ .select((value) => value!.trade?.payInExtraId)) ??
+ "Error",
+ ),
+ if (ref.watch(desktopExchangeModelProvider
+ .select((value) => value!.trade?.payInExtraId)) !=
+ null)
+ Container(
+ height: 1,
+ color: Theme.of(context).extension()!.background,
+ ),
DesktopStepItem(
label: "Amount",
value:
diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart
index 1a0a1f09f..64faa0d16 100644
--- a/lib/pages_desktop_specific/desktop_menu.dart
+++ b/lib/pages_desktop_specific/desktop_menu.dart
@@ -15,10 +15,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_menu_item.dart';
+import 'package:stackwallet/pages_desktop_specific/settings/settings_menu.dart';
import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/widgets/desktop/desktop_tor_status_button.dart';
import 'package:stackwallet/widgets/desktop/living_stack_icon.dart';
enum DesktopMenuItemId {
@@ -52,11 +54,10 @@ class _DesktopMenuState extends ConsumerState {
final Duration duration = const Duration(milliseconds: 250);
late final List controllers;
+ late final DMIController torButtonController;
double _width = expandedWidth;
- // final _buyDataLoadingService = BuyDataLoadingService();
-
void updateSelectedMenuItem(DesktopMenuItemId idKey) {
widget.onSelectionWillChange?.call(idKey);
@@ -72,6 +73,8 @@ class _DesktopMenuState extends ConsumerState {
e.toggle?.call();
}
+ torButtonController.toggle?.call();
+
setState(() {
_width = expanded ? minimizedWidth : expandedWidth;
});
@@ -91,6 +94,8 @@ class _DesktopMenuState extends ConsumerState {
DMIController(),
];
+ torButtonController = DMIController();
+
super.initState();
}
@@ -99,6 +104,8 @@ class _DesktopMenuState extends ConsumerState {
for (var e in controllers) {
e.dispose();
}
+ torButtonController.dispose();
+
super.dispose();
}
@@ -140,7 +147,26 @@ class _DesktopMenuState extends ConsumerState {
),
),
const SizedBox(
- height: 60,
+ height: 5,
+ ),
+ AnimatedContainer(
+ duration: duration,
+ width: _width == expandedWidth
+ ? _width - 32 // 16 padding on either side
+ : _width - 16, // 8 padding on either side
+ child: DesktopTorStatusButton(
+ transitionDuration: duration,
+ controller: torButtonController,
+ onPressed: () {
+ ref.read(currentDesktopMenuItemProvider.state).state =
+ DesktopMenuItemId.settings;
+ ref.watch(selectedSettingsMenuItemStateProvider.state).state =
+ 4;
+ },
+ ),
+ ),
+ const SizedBox(
+ height: 40,
),
Expanded(
child: AnimatedContainer(
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
index 292517699..22da0f217 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart
@@ -219,14 +219,20 @@ class _DesktopReceiveState extends ConsumerState {
if (coin != Coin.epicCash &&
coin != Coin.ethereum &&
coin != Coin.banano &&
- coin != Coin.nano)
+ coin != Coin.nano &&
+ coin != Coin.stellar &&
+ coin != Coin.stellarTestnet &&
+ coin != Coin.tezos)
const SizedBox(
height: 20,
),
if (coin != Coin.epicCash &&
coin != Coin.ethereum &&
coin != Coin.banano &&
- coin != Coin.nano)
+ coin != Coin.nano &&
+ coin != Coin.stellar &&
+ coin != Coin.stellarTestnet &&
+ coin != Coin.tezos)
SecondaryButton(
buttonHeight: ButtonHeight.l,
onPressed: generateNewAddress,
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
index 3d65eb6e2..f9f24cb76 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart
@@ -97,12 +97,16 @@ class _DesktopSendState extends ConsumerState {
late TextEditingController cryptoAmountController;
late TextEditingController baseAmountController;
// late TextEditingController feeController;
+ late TextEditingController memoController;
late final SendViewAutoFillData? _data;
final _addressFocusNode = FocusNode();
final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode();
+ final _memoFocus = FocusNode();
+
+ late final bool isStellar;
String? _note;
String? _onChainNote;
@@ -326,10 +330,12 @@ class _DesktopSendState extends ConsumerState {
},
);
} else {
+ final memo = isStellar ? memoController.text : null;
txDataFuture = manager.prepareSend(
address: _address!,
amount: amount,
args: {
+ "memo": memo,
"feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null,
"UTXOs": (manager.hasCoinControlSupport &&
@@ -663,6 +669,23 @@ class _DesktopSendState extends ConsumerState {
}
}
+ Future pasteMemo() async {
+ if (memoController.text.isNotEmpty) {
+ setState(() {
+ memoController.text = "";
+ });
+ } else {
+ final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain);
+ if (data?.text != null && data!.text!.isNotEmpty) {
+ String content = data.text!.trim();
+
+ setState(() {
+ memoController.text = content;
+ });
+ }
+ }
+ }
+
void fiatTextFieldOnChanged(String baseAmountString) {
final baseAmount = Amount.tryParseFiatString(
baseAmountString,
@@ -762,10 +785,12 @@ class _DesktopSendState extends ConsumerState {
coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
clipboard = widget.clipboard;
scanner = widget.barcodeScanner;
+ isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
sendToController = TextEditingController();
cryptoAmountController = TextEditingController();
baseAmountController = TextEditingController();
+ memoController = TextEditingController();
// feeController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged;
@@ -814,11 +839,13 @@ class _DesktopSendState extends ConsumerState {
sendToController.dispose();
cryptoAmountController.dispose();
baseAmountController.dispose();
+ memoController.dispose();
// feeController.dispose();
_addressFocusNode.dispose();
_cryptoFocus.dispose();
_baseFocus.dispose();
+ _memoFocus.dispose();
super.dispose();
}
@@ -1367,6 +1394,67 @@ class _DesktopSendState extends ConsumerState {
}
},
),
+ if (isStellar)
+ const SizedBox(
+ height: 10,
+ ),
+ if (isStellar)
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ minLines: 1,
+ maxLines: 5,
+ key: const Key("sendViewMemoFieldKey"),
+ controller: memoController,
+ readOnly: false,
+ autocorrect: false,
+ enableSuggestions: false,
+ focusNode: _memoFocus,
+ onChanged: (_) {
+ setState(() {});
+ },
+ style: STextStyles.desktopTextExtraSmall(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .textFieldActiveText,
+ height: 1.8,
+ ),
+ decoration: standardInputDecoration(
+ "Enter memo (optional)",
+ _memoFocus,
+ context,
+ desktopMed: true,
+ ).copyWith(
+ contentPadding: const EdgeInsets.only(
+ left: 16,
+ top: 11,
+ bottom: 12,
+ right: 5,
+ ),
+ suffixIcon: Padding(
+ padding: memoController.text.isEmpty
+ ? const EdgeInsets.only(right: 8)
+ : const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ TextFieldIconButton(
+ key: const Key("sendViewPasteMemoButtonKey"),
+ onTap: pasteMemo,
+ child: memoController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
if (!isPaynymSend)
const SizedBox(
height: 20,
diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
index b0225f286..f1c6093aa 100644
--- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
+++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
@@ -3,21 +3,23 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
-import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.dart';
+import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.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/prefs.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@@ -50,11 +52,16 @@ class _DesktopOrdinalDetailsViewState
late final UTXO? utxo;
Future _savePngToFile() async {
- final response = await get(Uri.parse(widget.ordinal.content));
+ HTTP client = HTTP();
- if (response.statusCode != 200) {
- throw Exception(
- "statusCode=${response.statusCode} body=${response.bodyBytes}");
+ final response = await client.get(
+ url: Uri.parse(widget.ordinal.content),
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+
+ if (response.code != 200) {
+ throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
}
final bytes = response.bodyBytes;
diff --git a/lib/pages_desktop_specific/settings/desktop_settings_view.dart b/lib/pages_desktop_specific/settings/desktop_settings_view.dart
index 2a8c180f9..6a785fc8a 100644
--- a/lib/pages_desktop_specific/settings/desktop_settings_view.dart
+++ b/lib/pages_desktop_specific/settings/desktop_settings_view.dart
@@ -19,6 +19,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/langua
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
+import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart';
import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@@ -56,7 +57,12 @@ class _DesktopSettingsViewState extends ConsumerState {
key: Key("settingsLanguageDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: LanguageOptionSettings.routeName,
- ), //language
+ ),
+ const Navigator(
+ key: Key("settingsTorDesktopKey"),
+ onGenerateRoute: RouteGenerator.generateRoute,
+ initialRoute: TorSettings.routeName,
+ ), //tor
const Navigator(
key: Key("settingsNodesDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
diff --git a/lib/pages_desktop_specific/settings/settings_menu.dart b/lib/pages_desktop_specific/settings/settings_menu.dart
index 4f3175a72..ba2c21781 100644
--- a/lib/pages_desktop_specific/settings/settings_menu.dart
+++ b/lib/pages_desktop_specific/settings/settings_menu.dart
@@ -32,6 +32,7 @@ class _SettingsMenuState extends ConsumerState {
"Security",
"Currency",
"Language",
+ "Tor settings",
"Nodes",
"Syncing preferences",
"Appearance",
diff --git a/lib/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart
new file mode 100644
index 000000000..212387035
--- /dev/null
+++ b/lib/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart
@@ -0,0 +1,409 @@
+/*
+ * This file is part of Stack Wallet.
+ *
+ * Copyright (c) 2023 Cypher Stack
+ * All Rights Reserved.
+ * The code is distributed under GPLv3 license, see LICENSE file for details.
+ * Generated by Cypher Stack on 2023-05-26
+ *
+ */
+
+import 'dart:async';
+
+import 'package:event_bus/event_bus.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/providers/global/prefs_provider.dart';
+import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/global_event_bus.dart';
+import 'package:stackwallet/services/tor_service.dart';
+import 'package:stackwallet/themes/stack_colors.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/utilities/util.dart';
+import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart';
+import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
+import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
+import 'package:stackwallet/widgets/desktop/secondary_button.dart';
+import 'package:stackwallet/widgets/rounded_white_container.dart';
+
+class TorSettings extends ConsumerStatefulWidget {
+ const TorSettings({Key? key}) : super(key: key);
+
+ static const String routeName = "/torDesktopSettings";
+
+ @override
+ ConsumerState createState() => _TorSettingsState();
+}
+
+class _TorSettingsState extends ConsumerState {
+ /// The global event bus.
+ EventBus eventBus = GlobalEventBus.instance;
+
+ /// Subscription to the TorConnectionStatusChangedEvent.
+ late StreamSubscription
+ _torConnectionStatusSubscription;
+
+ /// The current status of the Tor connection.
+ late TorConnectionStatus _torConnectionStatus;
+
+ /// Build the connect/disconnect button.
+ Widget _buildConnectButton(TorConnectionStatus status) {
+ switch (status) {
+ case TorConnectionStatus.disconnected:
+ return SecondaryButton(
+ label: "Connect to Tor",
+ width: 200,
+ buttonHeight: ButtonHeight.m,
+ onPressed: () async {
+ // Init the Tor service if it hasn't already been.
+ ref.read(pTorService).init();
+
+ // Start the Tor service.
+ try {
+ await ref.read(pTorService).start();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = true;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error starting tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ }
+ },
+ );
+ case TorConnectionStatus.connecting:
+ return AbsorbPointer(
+ child: SecondaryButton(
+ label: "Connecting to Tor",
+ width: 200,
+ buttonHeight: ButtonHeight.m,
+ onPressed: () {},
+ ),
+ );
+ case TorConnectionStatus.connected:
+ return SecondaryButton(
+ label: "Disconnect from Tor",
+ width: 200,
+ buttonHeight: ButtonHeight.m,
+ onPressed: () async {
+ // Stop the Tor service.
+ try {
+ await ref.read(pTorService).stop();
+
+ // Toggle the useTor preference on success.
+ ref.read(prefsChangeNotifierProvider).useTor = false;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Error stopping tor: $e\n$s",
+ level: LogLevel.Error,
+ );
+ }
+ },
+ );
+ }
+ }
+
+ @override
+ void initState() {
+ // Initialize the global event bus.
+ eventBus = GlobalEventBus.instance;
+
+ // Set the initial Tor connection status.
+ _torConnectionStatus = ref.read(pTorService).enabled
+ ? TorConnectionStatus.connected
+ : TorConnectionStatus.disconnected;
+
+ // Subscribe to the TorConnectionStatusChangedEvent.
+ _torConnectionStatusSubscription =
+ eventBus.on().listen(
+ (event) async {
+ // Rebuild the widget.
+ setState(() {
+ _torConnectionStatus = event.newStatus;
+ });
+ },
+ );
+
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ // Clean up the TorConnectionStatusChangedEvent subscription.
+ _torConnectionStatusSubscription.cancel();
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final isDesktop = Util.isDesktop;
+
+ /// todo: redo the padding
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ right: 30,
+ ),
+ child: RoundedWhiteContainer(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SvgPicture.asset(
+ Assets.svg.circleTor,
+ width: 48,
+ height: 48,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SvgPicture.asset(
+ _torConnectionStatus == TorConnectionStatus.connected
+ ? Assets.svg.connectedButton
+ : _torConnectionStatus ==
+ TorConnectionStatus.connecting
+ ? Assets.svg.connectingButton
+ : Assets.svg.disconnectedButton,
+ width: 48,
+ height: 48,
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.all(10),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Tor settings",
+ style: STextStyles.desktopTextSmall(context),
+ ),
+ RichText(
+ textAlign: TextAlign.start,
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text:
+ "\nConnect to the Tor Network with one click.",
+ style: STextStyles.desktopTextExtraExtraSmall(
+ context),
+ ),
+ TextSpan(
+ text: "\tWhat is Tor?",
+ style: STextStyles.richLink(context).copyWith(
+ fontSize: 14,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return DesktopDialog(
+ maxWidth: 580,
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.end,
+ children: [
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ Navigator.of(context)
+ .pop(true),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Text(
+ "What is Tor?",
+ style:
+ STextStyles.desktopH2(
+ context),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ Text(
+ "Short for \"The Onion Router\", is an open-source software that enables internet communication"
+ " to remain anonymous by routing internet traffic through a series of layered nodes,"
+ " to obscure the origin and destination of data.",
+ style: STextStyles
+ .desktopTextMedium(
+ context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension<
+ StackColors>()!
+ .textDark3,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: _buildConnectButton(_torConnectionStatus),
+ ),
+ const SizedBox(
+ height: 30,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(10.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ RichText(
+ textAlign: TextAlign.start,
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: "Tor killswitch",
+ style: STextStyles.desktopTextExtraExtraSmall(
+ context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .textDark),
+ ),
+ TextSpan(
+ text: "\nWhat is Tor killswitch?",
+ style: STextStyles.richLink(context).copyWith(
+ fontSize: 14,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return DesktopDialog(
+ maxWidth: 580,
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment:
+ MainAxisAlignment.end,
+ children: [
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ Navigator.of(context)
+ .pop(true),
+ ),
+ ],
+ ),
+ Padding(
+ padding:
+ const EdgeInsets.all(20),
+ child: Column(
+ mainAxisSize:
+ MainAxisSize.max,
+ children: [
+ Text(
+ "What is Tor killswitch?",
+ style: STextStyles
+ .desktopH2(context),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ Text(
+ "A security feature that protects your information from accidental exposure by"
+ " disconnecting your device from the Tor network if the"
+ " connection is disrupted or compromised.",
+ style: STextStyles
+ .desktopTextMedium(
+ context)
+ .copyWith(
+ color: Theme.of(
+ context)
+ .extension<
+ StackColors>()!
+ .textDark3,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(right: 8.0),
+ child: SizedBox(
+ height: 20,
+ width: 40,
+ child: DraggableSwitchButton(
+ isOn: ref.watch(
+ prefsChangeNotifierProvider
+ .select((value) => value.torKillSwitch),
+ ),
+ onValueChanged: (newValue) {
+ ref
+ .read(prefsChangeNotifierProvider)
+ .torKillSwitch = newValue;
+ },
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/providers/global/http_provider.dart b/lib/providers/global/http_provider.dart
new file mode 100644
index 000000000..84f8793c0
--- /dev/null
+++ b/lib/providers/global/http_provider.dart
@@ -0,0 +1,4 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:stackwallet/networking/http.dart';
+
+final pHTTP = Provider((ref) => HTTP());
diff --git a/lib/route_generator.dart b/lib/route_generator.dart
index be744f583..4c52bc236 100644
--- a/lib/route_generator.dart
+++ b/lib/route_generator.dart
@@ -27,6 +27,7 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_to
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
+import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
@@ -110,6 +111,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
+import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
@@ -167,6 +169,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/langua
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
+import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart';
import 'package:stackwallet/services/coins/manager.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
@@ -683,6 +686,18 @@ class RouteGenerator {
builder: (_) => const LanguageSettingsView(),
settings: RouteSettings(name: settings.name));
+ case TorSettingsView.routeName:
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => const TorSettingsView(),
+ settings: RouteSettings(name: settings.name));
+
+ case TorSettings.routeName:
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => const TorSettings(),
+ settings: RouteSettings(name: settings.name));
+
case AboutView.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
@@ -1131,6 +1146,21 @@ class RouteGenerator {
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
+ case NewWalletOptionsView.routeName:
+ if (args is Tuple2) {
+ return getRoute(
+ shouldUseMaterialRoute: useMaterialPageRoute,
+ builder: (_) => NewWalletOptionsView(
+ walletName: args.item1,
+ coin: args.item2,
+ ),
+ settings: RouteSettings(
+ name: settings.name,
+ ),
+ );
+ }
+ return _routeError("${settings.name} invalid args: ${args.toString()}");
+
case RestoreWalletView.routeName:
if (args is Tuple5) {
return getRoute(
diff --git a/lib/services/buy/simplex/simplex_api.dart b/lib/services/buy/simplex/simplex_api.dart
index 8158c6b8c..1b2d26e26 100644
--- a/lib/services/buy/simplex/simplex_api.dart
+++ b/lib/services/buy/simplex/simplex_api.dart
@@ -12,12 +12,13 @@ import 'dart:async';
import 'dart:convert';
import 'package:decimal/decimal.dart';
-import 'package:http/http.dart' as http;
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
import 'package:stackwallet/models/buy/response_objects/order.dart';
import 'package:stackwallet/models/buy/response_objects/quote.dart';
+import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/buy/buy_response.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fiat_enum.dart';
import 'package:stackwallet/utilities/logger.dart';
@@ -35,8 +36,7 @@ class SimplexAPI {
static final SimplexAPI _instance = SimplexAPI._();
static SimplexAPI get instance => _instance;
- /// set this to override using standard http client. Useful for testing
- http.Client? client;
+ HTTP client = HTTP();
Uri _buildUri(String path, Map? params) {
if (scheme == "http") {
@@ -55,10 +55,15 @@ class SimplexAPI {
};
Uri url = _buildUri('api.php', data);
- var res = await http.post(url, headers: headers);
- if (res.statusCode != 200) {
+ var res = await client.post(
+ url: url,
+ headers: headers,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+ if (res.code != 200) {
throw Exception(
- 'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
+ 'getAvailableCurrencies exception: statusCode= ${res.code}');
}
final jsonArray = jsonDecode(res.body); // TODO handle if invalid json
@@ -116,10 +121,15 @@ class SimplexAPI {
};
Uri url = _buildUri('api.php', data);
- var res = await http.post(url, headers: headers);
- if (res.statusCode != 200) {
+ var res = await client.post(
+ url: url,
+ headers: headers,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+ if (res.code != 200) {
throw Exception(
- 'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
+ 'getAvailableCurrencies exception: statusCode= ${res.code}');
}
final jsonArray = jsonDecode(res.body); // TODO validate json
@@ -192,9 +202,14 @@ class SimplexAPI {
}
Uri url = _buildUri('api.php', data);
- var res = await http.get(url, headers: headers);
- if (res.statusCode != 200) {
- throw Exception('getQuote exception: statusCode= ${res.statusCode}');
+ var res = await client.get(
+ url: url,
+ headers: headers,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+ if (res.code != 200) {
+ throw Exception('getQuote exception: statusCode= ${res.code}');
}
final jsonArray = jsonDecode(res.body);
if (jsonArray.containsKey('error') as bool) {
@@ -294,9 +309,14 @@ class SimplexAPI {
}
Uri url = _buildUri('api.php', data);
- var res = await http.get(url, headers: headers);
- if (res.statusCode != 200) {
- throw Exception('newOrder exception: statusCode= ${res.statusCode}');
+ var res = await client.get(
+ url: url,
+ headers: headers,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
+ if (res.code != 200) {
+ throw Exception('newOrder exception: statusCode= ${res.code}');
}
final jsonArray = jsonDecode(res.body); // TODO check if valid json
if (jsonArray.containsKey('error') as bool) {
diff --git a/lib/services/coins/banano/banano_wallet.dart b/lib/services/coins/banano/banano_wallet.dart
index e2032aa09..9f314f3a3 100644
--- a/lib/services/coins/banano/banano_wallet.dart
+++ b/lib/services/coins/banano/banano_wallet.dart
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:convert';
-import 'package:http/http.dart' as http;
import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/db/hive/db.dart';
@@ -10,6 +9,7 @@ import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
+import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
@@ -19,6 +19,7 @@ import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart';
+import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart';
@@ -145,10 +146,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
+ HTTP client = HTTP();
+
Future requestWork(String hash) async {
- return http
+ return client
.post(
- Uri.parse("https://rpc.nano.to"), // this should be a
+ url: Uri.parse("https://rpc.nano.to"), // this should be a
headers: {'Content-type': 'application/json'},
body: json.encode(
{
@@ -156,17 +159,19 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"hash": hash,
},
),
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
)
- .then((http.Response response) {
- if (response.statusCode == 200) {
+ .then((client) {
+ if (client.code == 200) {
final Map decoded =
- json.decode(response.body) as Map;
+ json.decode(client.body) as Map;
if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}");
}
return decoded["work"] as String?;
} else {
- throw Exception("Received error ${response.statusCode}");
+ throw Exception("Received error ${client.code}");
}
});
}
@@ -185,10 +190,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = {
"Content-Type": "application/json",
};
- final balanceResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final balanceResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: balanceBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final balanceData = jsonDecode(balanceResponse.body);
@@ -203,10 +210,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true",
"account": publicAddress,
});
- final infoResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final infoResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: infoBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final String frontier =
@@ -256,10 +265,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "send",
"block": sendBlock,
});
- final processResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final processResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: processBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final Map decoded =
@@ -328,8 +339,13 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = {
"Content-Type": "application/json",
};
- final response = await http.post(Uri.parse(getCurrentNode().host),
- headers: headers, body: body);
+ final response = await client.post(
+ url: Uri.parse(getCurrentNode().host),
+ headers: headers,
+ body: body,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
final data = jsonDecode(response.body);
_balance = Balance(
total: Amount(
@@ -367,10 +383,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true",
"account": publicAddress,
});
- final infoResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final infoResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: infoBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final infoData = jsonDecode(infoResponse.body);
@@ -385,10 +403,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"account": publicAddress,
});
- final balanceResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final balanceResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: balanceBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final balanceData = jsonDecode(balanceResponse.body);
@@ -458,10 +478,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "receive",
"block": receiveBlock,
});
- final processResponse = await http.post(
- Uri.parse(getCurrentNode().host),
+ final processResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
headers: headers,
body: processBody,
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final Map decoded =
@@ -472,14 +494,18 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
Future confirmAllReceivable() async {
- final receivableResponse = await http.post(Uri.parse(getCurrentNode().host),
- headers: {"Content-Type": "application/json"},
- body: jsonEncode({
- "action": "receivable",
- "source": "true",
- "account": await currentReceivingAddress,
- "count": "-1",
- }));
+ final receivableResponse = await client.post(
+ url: Uri.parse(getCurrentNode().host),
+ headers: {"Content-Type": "application/json"},
+ body: jsonEncode({
+ "action": "receivable",
+ "source": "true",
+ "account": await currentReceivingAddress,
+ "count": "-1",
+ }),
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
final receivableData = await jsonDecode(receivableResponse.body);
if (receivableData["blocks"] == "") {
@@ -501,13 +527,17 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
await confirmAllReceivable();
final receivingAddress = (await _currentReceivingAddress)!;
final String publicAddress = receivingAddress.value;
- final response = await http.post(Uri.parse(getCurrentNode().host),
- headers: {"Content-Type": "application/json"},
- body: jsonEncode({
- "action": "account_history",
- "account": publicAddress,
- "count": "-1",
- }));
+ final response = await client.post(
+ url: Uri.parse(getCurrentNode().host),
+ headers: {"Content-Type": "application/json"},
+ body: jsonEncode({
+ "action": "account_history",
+ "account": publicAddress,
+ "count": "-1",
+ }),
+ proxyInfo:
+ Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
+ );
final data = await jsonDecode(response.body);
final transactions =
data["history"] is List ? data["history"] as List : [];
@@ -600,7 +630,9 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
}
@override
- Future initializeNew() async {
+ Future