Merge arti into fusion

This commit is contained in:
Josh Babb 2023-09-14 14:45:47 -05:00
commit 67f3217daf
188 changed files with 13039 additions and 6461 deletions

6
.gitignore vendored
View file

@ -55,4 +55,10 @@ libcw_monero.dll
libcw_wownero.dll libcw_wownero.dll
libepic_cash_wallet.dll libepic_cash_wallet.dll
libmobileliblelantus.dll libmobileliblelantus.dll
libtor_ffi.dll
/libisar.so /libisar.so
libtor_ffi.so
tor_logs.txt
torrc

5
.gitmodules vendored
View file

@ -7,6 +7,9 @@
[submodule "crypto_plugins/flutter_liblelantus"] [submodule "crypto_plugins/flutter_liblelantus"]
path = crypto_plugins/flutter_liblelantus path = crypto_plugins/flutter_liblelantus
url = https://github.com/cypherstack/flutter_liblelantus.git 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"] [submodule "fusiondart"]
path = fusiondart path = fusiondart
url = https://github.com/cypherstack/fusiondart url = https://github.com/cypherstack/fusiondart.git

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,5 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="24" fill="#E0E3E3"/>
<path d="M19.0284 34.8483C18.826 35.4564 19.1553 36.1134 19.7627 36.3159C19.884 36.3565 20.0097 36.3759 20.1306 36.3759C20.6169 36.3759 21.0689 36.0685 21.2318 35.5826L21.8017 33.8723C21.0171 33.7401 20.2679 33.5325 19.5553 33.27L19.0284 34.8483ZM28.9671 34.8483L28.4412 33.2695C27.7286 33.5322 26.9789 33.7398 26.1948 33.8718L26.7647 35.5821C26.9272 36.0665 27.3767 36.3759 27.865 36.3759C27.9858 36.3759 28.1097 36.357 28.232 36.3162C28.8414 36.1148 29.1266 35.4139 28.9671 34.8483ZM22.8376 34.0024V35.2157C22.8376 35.8586 23.3597 36.3759 23.9978 36.3759C24.6359 36.3759 25.1579 35.8562 25.1579 35.2157V34.0033C24.776 34.0362 24.3893 34.0555 23.9978 34.0555C23.6062 34.0555 23.2195 34.0362 22.8376 34.0024Z" fill="black"/>
<path d="M30.3295 18.0837C33.1429 19.7321 34.8203 22.4053 34.8203 25.2332C34.8203 30.0971 29.9622 34.0552 23.9922 34.0552C18.0222 34.0552 13.1641 30.0962 13.1641 25.2332C13.1641 22.4053 14.8439 19.7321 17.6568 18.0837C17.657 18.0836 17.6575 18.0833 17.6582 18.0828C17.7403 18.033 20.8389 16.1495 21.8067 12.2147C21.8744 11.9329 22.0951 11.7139 22.3776 11.6467C22.6532 11.5787 22.9529 11.6746 23.1414 11.8944L23.9922 12.8833L24.8449 11.8943C25.032 11.6745 25.3356 11.5785 25.6106 11.6465C25.893 11.7137 26.1135 11.9328 26.1815 12.2145C27.1555 16.1847 30.2978 18.0656 30.3295 18.0837Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#00A578"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#00A578"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#F4C517"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#F4C517"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

4
assets/svg/tor.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.31521 18.0367C6.16525 18.4871 6.4092 18.9737 6.85912 19.1238C6.949 19.1538 7.04209 19.1682 7.13161 19.1682C7.49183 19.1682 7.82663 18.9405 7.9473 18.5806L8.36947 17.3137C7.78832 17.2157 7.23331 17.062 6.70551 16.8675L6.31521 18.0367ZM13.6772 18.0367L13.2876 16.8672C12.7598 17.0618 12.2044 17.2156 11.6236 17.3133L12.0458 18.5802C12.1661 18.939 12.4991 19.1682 12.8608 19.1682C12.9503 19.1682 13.042 19.1542 13.1327 19.124C13.5841 18.9748 13.7953 18.4556 13.6772 18.0367ZM9.13682 17.41V18.3088C9.13682 18.785 9.52354 19.1682 9.9962 19.1682C10.4689 19.1682 10.8556 18.7832 10.8556 18.3088V17.4107C10.5727 17.4351 10.2862 17.4494 9.9962 17.4494C9.70616 17.4494 9.4197 17.4351 9.13682 17.41Z" fill="#C4C7C7"/>
<path d="M14.6878 5.61724C16.7718 6.83827 18.0143 8.81841 18.0143 10.9131C18.0143 14.5161 14.4157 17.448 9.99349 17.448C5.57129 17.448 1.97266 14.5154 1.97266 10.9131C1.97266 8.81841 3.21696 6.83827 5.30059 5.61724C5.30078 5.61712 5.30113 5.61691 5.30164 5.6166C5.36244 5.57964 7.6577 4.18451 8.37464 1.26979C8.42477 1.0611 8.58827 0.898861 8.79753 0.849054C9.00163 0.798701 9.22363 0.869768 9.36328 1.03255L9.99349 1.76509L10.6251 1.03245C10.7637 0.869643 10.9886 0.798597 11.1923 0.848943C11.4015 0.898715 11.5649 1.06099 11.6152 1.26968C12.3367 4.21053 14.6643 5.60381 14.6878 5.61724Z" fill="#C4C7C7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
crypto_plugins/tor Submodule

@ -0,0 +1 @@
Subproject commit a819223b23e9fa1d76bde82ed9109651e96f2ad3

View file

@ -123,7 +123,7 @@ flutter run android
Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work
#### Linux #### 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 pub get
flutter run linux flutter run linux

View file

@ -9,7 +9,6 @@
*/ */
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart';
@ -158,6 +157,7 @@ class CachedElectrumX {
Future<List<String>> getUsedCoinSerials({ Future<List<String>> getUsedCoinSerials({
required Coin coin, required Coin coin,
int startNumber = 0,
}) async { }) async {
try { try {
final box = await DB.instance.getUsedSerialsCacheBox(coin: coin); final box = await DB.instance.getUsedSerialsCacheBox(coin: coin);
@ -168,7 +168,7 @@ class CachedElectrumX {
_list == null ? {} : List<String>.from(_list).toSet(); _list == null ? {} : List<String>.from(_list).toSet();
final startNumber = 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( final serials = await electrumXClient.getUsedCoinSerials(
startNumber: startNumber, startNumber: startNumber,

View file

@ -8,13 +8,20 @@
* *
*/ */
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.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/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.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/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -65,12 +72,27 @@ class ElectrumX {
JsonRPC? _rpcClient; JsonRPC? _rpcClient;
late Prefs _prefs; late Prefs _prefs;
late TorService _torService;
List<ElectrumXNode>? failovers; List<ElectrumXNode>? failovers;
int currentFailoverIndex = -1; int currentFailoverIndex = -1;
final Duration connectionTimeoutForSpecialCaseJsonRPCClients; final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
// add finalizer to cancel stream subscription when all references to an
// instance of ElectrumX becomes inaccessible
static final Finalizer<ElectrumX> _finalizer = Finalizer(
(p0) {
p0._torPreferenceListener?.cancel();
p0._torStatusListener?.cancel();
},
);
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
final Mutex _torConnectingLock = Mutex();
bool _requireMutex = false;
ElectrumX({ ElectrumX({
required String host, required String host,
required int port, required int port,
@ -80,26 +102,79 @@ class ElectrumX {
JsonRPC? client, JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients = this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60), const Duration(seconds: 60),
TorService? torService,
EventBus? globalEventBusForTesting,
}) { }) {
_prefs = prefs; _prefs = prefs;
_torService = torService ?? TorService.sharedInstance;
_host = host; _host = host;
_port = port; _port = port;
_useSSL = useSSL; _useSSL = useSSL;
_rpcClient = client; _rpcClient = client;
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().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<TorPreferenceChangedEvent>().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({ factory ElectrumX.from({
required ElectrumXNode node, required ElectrumXNode node,
required Prefs prefs, required Prefs prefs,
required List<ElectrumXNode> failovers, required List<ElectrumXNode> failovers,
}) => TorService? torService,
ElectrumX( EventBus? globalEventBusForTesting,
}) {
return ElectrumX(
host: node.address, host: node.address,
port: node.port, port: node.port,
useSSL: node.useSSL, useSSL: node.useSSL,
prefs: prefs, prefs: prefs,
torService: torService,
failovers: failovers, failovers: failovers,
globalEventBusForTesting: globalEventBusForTesting,
); );
}
Future<bool> _allow() async { Future<bool> _allow() async {
if (_prefs.wifiOnly) { if (_prefs.wifiOnly) {
@ -109,6 +184,75 @@ class ElectrumX {
return true; 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 /// Send raw rpc command
Future<dynamic> request({ Future<dynamic> request({
required String command, required String command,
@ -121,20 +265,10 @@ class ElectrumX {
throw WifiOnlyException(); throw WifiOnlyException();
} }
if (currentFailoverIndex == -1) { if (_requireMutex) {
_rpcClient ??= JsonRPC( await _torConnectingLock.protect(() async => _checkRpcClient());
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} else { } else {
_rpcClient = JsonRPC( _checkRpcClient();
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} }
try { try {
@ -221,20 +355,10 @@ class ElectrumX {
throw WifiOnlyException(); throw WifiOnlyException();
} }
if (currentFailoverIndex == -1) { if (_requireMutex) {
_rpcClient ??= JsonRPC( await _torConnectingLock.protect(() async => _checkRpcClient());
host: host,
port: port,
useSSL: useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} else { } else {
_rpcClient = JsonRPC( _checkRpcClient();
host: failovers![currentFailoverIndex].address,
port: failovers![currentFailoverIndex].port,
useSSL: failovers![currentFailoverIndex].useSSL,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
);
} }
try { try {

View file

@ -14,7 +14,10 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.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/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
// Json RPC class to handle connecting to electrumx servers // Json RPC class to handle connecting to electrumx servers
class JsonRPC { class JsonRPC {
@ -23,16 +26,19 @@ class JsonRPC {
required this.port, required this.port,
this.useSSL = false, this.useSSL = false,
this.connectionTimeout = const Duration(seconds: 60), this.connectionTimeout = const Duration(seconds: 60),
required ({InternetAddress host, int port})? proxyInfo,
}); });
final bool useSSL; final bool useSSL;
final String host; final String host;
final int port; final int port;
final Duration connectionTimeout; final Duration connectionTimeout;
({InternetAddress host, int port})? proxyInfo;
final _requestMutex = Mutex(); final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue(); final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket; Socket? _socket;
StreamSubscription<Uint8List>? _subscription; SOCKSSocket? _socksSocket;
StreamSubscription<List<int>>? _subscription;
void _dataHandler(List<int> data) { void _dataHandler(List<int> data) {
_requestQueue.nextIncompleteReq.then((req) { _requestQueue.nextIncompleteReq.then((req) {
@ -75,7 +81,12 @@ class JsonRPC {
_requestQueue.nextIncompleteReq.then((req) { _requestQueue.nextIncompleteReq.then((req) {
if (req != null) { if (req != null) {
// \r\n required by electrumx server // \r\n required by electrumx server
if (_socket != null) {
_socket!.write('${req.jsonRequest}\r\n'); _socket!.write('${req.jsonRequest}\r\n');
}
if (_socksSocket != null) {
_socksSocket!.write('${req.jsonRequest}\r\n');
}
// TODO different timeout length? // TODO different timeout length?
req.initiateTimeout( req.initiateTimeout(
@ -92,6 +103,7 @@ class JsonRPC {
Duration requestTimeout, Duration requestTimeout,
) async { ) async {
await _requestMutex.protect(() async { await _requestMutex.protect(() async {
if (!Prefs.instance.useTor) {
if (_socket == null) { if (_socket == null) {
Logging.instance.log( Logging.instance.log(
"JsonRPC request: opening socket $host:$port", "JsonRPC request: opening socket $host:$port",
@ -99,6 +111,15 @@ class JsonRPC {
); );
await connect(); await connect();
} }
} else {
if (_socksSocket == null) {
Logging.instance.log(
"JsonRPC request: opening SOCKS socket to $host:$port",
level: LogLevel.Info,
);
await connect();
}
}
}); });
final req = _JsonRPCRequest( final req = _JsonRPCRequest(
@ -113,9 +134,9 @@ class JsonRPC {
reason: "return req.completer.future.onError: $error\n$stackTrace", reason: "return req.completer.future.onError: $error\n$stackTrace",
); );
return JsonRPCResponse( return JsonRPCResponse(
exception: error is Exception exception: error is JsonRpcException
? error ? error
: Exception( : JsonRpcException(
"req.completer.future.onError: $error\n$stackTrace", "req.completer.future.onError: $error\n$stackTrace",
), ),
); );
@ -137,6 +158,8 @@ class JsonRPC {
_subscription = null; _subscription = null;
_socket?.destroy(); _socket?.destroy();
_socket = null; _socket = null;
await _socksSocket?.close();
_socksSocket = null;
// clean up remaining queue // clean up remaining queue
await _requestQueue.completeRemainingWithError( await _requestQueue.completeRemainingWithError(
@ -146,12 +169,7 @@ class JsonRPC {
} }
Future<void> connect() async { Future<void> connect() async {
if (_socket != null) { if (!Prefs.instance.useTor) {
throw Exception(
"JsonRPC attempted to connect to an already existing socket!",
);
}
if (useSSL) { if (useSSL) {
_socket = await SecureSocket.connect( _socket = await SecureSocket.connect(
host, host,
@ -173,6 +191,62 @@ class JsonRPC {
onDone: _doneHandler, onDone: _doneHandler,
cancelOnError: true, cancelOnError: true,
); );
} else {
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,
);
}
} }
} }
@ -277,7 +351,7 @@ class _JsonRPCRequest {
Future<void>.delayed(requestTimeout).then((_) { Future<void>.delayed(requestTimeout).then((_) {
if (!isComplete) { if (!isComplete) {
try { try {
throw Exception("_JsonRPCRequest timed out: $jsonRequest"); throw JsonRpcException("_JsonRPCRequest timed out: $jsonRequest");
} catch (e, s) { } catch (e, s) {
completer.completeError(e, s); completer.completeError(e, s);
onTimedOut?.call(); onTimedOut?.call();
@ -291,7 +365,18 @@ class _JsonRPCRequest {
class JsonRPCResponse { class JsonRPCResponse {
final dynamic data; final dynamic data;
final Exception? exception; final JsonRpcException? exception;
JsonRPCResponse({this.data, this.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;
}
}

View file

@ -1,324 +1,324 @@
/* // /*
* This file is part of Stack Wallet. // * This file is part of Stack Wallet.
* // *
* Copyright (c) 2023 Cypher Stack // * Copyright (c) 2023 Cypher Stack
* All Rights Reserved. // * All Rights Reserved.
* The code is distributed under GPLv3 license, see LICENSE file for details. // * The code is distributed under GPLv3 license, see LICENSE file for details.
* Generated by Cypher Stack on 2023-05-26 // * Generated by Cypher Stack on 2023-05-26
* // *
*/ // */
//
import 'dart:async'; // import 'dart:async';
import 'dart:convert'; // import 'dart:convert';
import 'dart:io'; // import 'dart:io';
//
import 'package:flutter/foundation.dart'; // import 'package:flutter/foundation.dart';
import 'package:stackwallet/utilities/logger.dart'; // import 'package:stackwallet/utilities/logger.dart';
//
class ElectrumXSubscription with ChangeNotifier { // class ElectrumXSubscription with ChangeNotifier {
dynamic _response; // dynamic _response;
dynamic get response => _response; // dynamic get response => _response;
set response(dynamic newData) { // set response(dynamic newData) {
_response = newData; // _response = newData;
notifyListeners(); // notifyListeners();
} // }
} // }
//
class SocketTask { // class SocketTask {
SocketTask({this.completer, this.subscription}); // SocketTask({this.completer, this.subscription});
//
final Completer<dynamic>? completer; // final Completer<dynamic>? completer;
final ElectrumXSubscription? subscription; // final ElectrumXSubscription? subscription;
//
bool get isSubscription => subscription != null; // bool get isSubscription => subscription != null;
} // }
//
class SubscribableElectrumXClient { // class SubscribableElectrumXClient {
int _currentRequestID = 0; // int _currentRequestID = 0;
bool _isConnected = false; // bool _isConnected = false;
List<int> _responseData = []; // List<int> _responseData = [];
final Map<String, SocketTask> _tasks = {}; // final Map<String, SocketTask> _tasks = {};
Timer? _aliveTimer; // Timer? _aliveTimer;
Socket? _socket; // Socket? _socket;
late final bool _useSSL; // late final bool _useSSL;
late final Duration _connectionTimeout; // late final Duration _connectionTimeout;
late final Duration _keepAlive; // late final Duration _keepAlive;
//
bool get isConnected => _isConnected; // bool get isConnected => _isConnected;
bool get useSSL => _useSSL; // bool get useSSL => _useSSL;
//
void Function(bool)? onConnectionStatusChanged; // void Function(bool)? onConnectionStatusChanged;
//
SubscribableElectrumXClient({ // SubscribableElectrumXClient({
bool useSSL = true, // bool useSSL = true,
this.onConnectionStatusChanged, // this.onConnectionStatusChanged,
Duration connectionTimeout = const Duration(seconds: 5), // Duration connectionTimeout = const Duration(seconds: 5),
Duration keepAlive = const Duration(seconds: 10), // Duration keepAlive = const Duration(seconds: 10),
}) { // }) {
_useSSL = useSSL; // _useSSL = useSSL;
_connectionTimeout = connectionTimeout; // _connectionTimeout = connectionTimeout;
_keepAlive = keepAlive; // _keepAlive = keepAlive;
} // }
//
Future<void> connect({required String host, required int port}) async { // Future<void> connect({required String host, required int port}) async {
try { // try {
await _socket?.close(); // await _socket?.close();
} catch (_) {} // } catch (_) {}
//
if (_useSSL) { // if (_useSSL) {
_socket = await SecureSocket.connect( // _socket = await SecureSocket.connect(
host, // host,
port, // port,
timeout: _connectionTimeout, // timeout: _connectionTimeout,
onBadCertificate: (_) => true, // onBadCertificate: (_) => true,
); // );
} else { // } else {
_socket = await Socket.connect( // _socket = await Socket.connect(
host, // host,
port, // port,
timeout: _connectionTimeout, // timeout: _connectionTimeout,
); // );
} // }
_updateConnectionStatus(true); // _updateConnectionStatus(true);
//
_socket!.listen( // _socket!.listen(
_dataHandler, // _dataHandler,
onError: _errorHandler, // onError: _errorHandler,
onDone: _doneHandler, // onDone: _doneHandler,
cancelOnError: true, // cancelOnError: true,
); // );
//
_aliveTimer?.cancel(); // _aliveTimer?.cancel();
_aliveTimer = Timer.periodic( // _aliveTimer = Timer.periodic(
_keepAlive, // _keepAlive,
(_) async => _updateConnectionStatus(await ping()), // (_) async => _updateConnectionStatus(await ping()),
); // );
} // }
//
Future<void> disconnect() async { // Future<void> disconnect() async {
_aliveTimer?.cancel(); // _aliveTimer?.cancel();
await _socket?.close(); // await _socket?.close();
onConnectionStatusChanged = null; // onConnectionStatusChanged = null;
} // }
//
String _buildJsonRequestString({ // String _buildJsonRequestString({
required String method, // required String method,
required String id, // required String id,
required List<dynamic> params, // required List<dynamic> params,
}) { // }) {
final paramString = jsonEncode(params); // final paramString = jsonEncode(params);
return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n'; // return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
} // }
//
void _updateConnectionStatus(bool connectionStatus) { // void _updateConnectionStatus(bool connectionStatus) {
if (_isConnected != connectionStatus && onConnectionStatusChanged != null) { // if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
onConnectionStatusChanged!(connectionStatus); // onConnectionStatusChanged!(connectionStatus);
} // }
_isConnected = connectionStatus; // _isConnected = connectionStatus;
} // }
//
void _dataHandler(List<int> data) { // void _dataHandler(List<int> data) {
_responseData.addAll(data); // _responseData.addAll(data);
//
// 0x0A is newline // // 0x0A is newline
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html // // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
if (data.last == 0x0A) { // if (data.last == 0x0A) {
try { // try {
final response = jsonDecode(String.fromCharCodes(_responseData)) // final response = jsonDecode(String.fromCharCodes(_responseData))
as Map<String, dynamic>; // as Map<String, dynamic>;
_responseHandler(response); // _responseHandler(response);
} catch (e, s) { // } catch (e, s) {
Logging.instance // Logging.instance
.log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error); // .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
rethrow; // rethrow;
} finally { // } finally {
_responseData = []; // _responseData = [];
} // }
} // }
} // }
//
void _responseHandler(Map<String, dynamic> response) { // void _responseHandler(Map<String, dynamic> response) {
// subscriptions will have a method in the response // // subscriptions will have a method in the response
if (response['method'] is String) { // if (response['method'] is String) {
_subscriptionHandler(response: response); // _subscriptionHandler(response: response);
return; // return;
} // }
//
final id = response['id'] as String; // final id = response['id'] as String;
final result = response['result']; // final result = response['result'];
//
_complete(id, result); // _complete(id, result);
} // }
//
void _subscriptionHandler({ // void _subscriptionHandler({
required Map<String, dynamic> response, // required Map<String, dynamic> response,
}) { // }) {
final method = response['method']; // final method = response['method'];
switch (method) { // switch (method) {
case "blockchain.scripthash.subscribe": // case "blockchain.scripthash.subscribe":
final params = response["params"] as List<dynamic>; // final params = response["params"] as List<dynamic>;
final scripthash = params.first as String; // final scripthash = params.first as String;
final taskId = "blockchain.scripthash.subscribe:$scripthash"; // final taskId = "blockchain.scripthash.subscribe:$scripthash";
//
_tasks[taskId]?.subscription?.response = params.last; // _tasks[taskId]?.subscription?.response = params.last;
break; // break;
case "blockchain.headers.subscribe": // case "blockchain.headers.subscribe":
final params = response["params"]; // final params = response["params"];
const taskId = "blockchain.headers.subscribe"; // const taskId = "blockchain.headers.subscribe";
//
_tasks[taskId]?.subscription?.response = params.first; // _tasks[taskId]?.subscription?.response = params.first;
break; // break;
default: // default:
break; // break;
} // }
} // }
//
void _errorHandler(Object error, StackTrace trace) { // void _errorHandler(Object error, StackTrace trace) {
_updateConnectionStatus(false); // _updateConnectionStatus(false);
Logging.instance.log( // Logging.instance.log(
"SubscribableElectrumXClient called _errorHandler with: $error\n$trace", // "SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
level: LogLevel.Info); // level: LogLevel.Info);
} // }
//
void _doneHandler() { // void _doneHandler() {
_updateConnectionStatus(false); // _updateConnectionStatus(false);
Logging.instance.log("SubscribableElectrumXClient called _doneHandler", // Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
level: LogLevel.Info); // level: LogLevel.Info);
} // }
//
void _complete(String id, dynamic data) { // void _complete(String id, dynamic data) {
if (_tasks[id] == null) { // if (_tasks[id] == null) {
return; // return;
} // }
//
if (!(_tasks[id]?.completer?.isCompleted ?? false)) { // if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
_tasks[id]?.completer?.complete(data); // _tasks[id]?.completer?.complete(data);
} // }
//
if (!(_tasks[id]?.isSubscription ?? false)) { // if (!(_tasks[id]?.isSubscription ?? false)) {
_tasks.remove(id); // _tasks.remove(id);
} else { // } else {
_tasks[id]?.subscription?.response = data; // _tasks[id]?.subscription?.response = data;
} // }
} // }
//
void _addTask({ // void _addTask({
required String id, // required String id,
required Completer<dynamic> completer, // required Completer<dynamic> completer,
}) { // }) {
_tasks[id] = SocketTask(completer: completer, subscription: null); // _tasks[id] = SocketTask(completer: completer, subscription: null);
} // }
//
void _addSubscriptionTask({ // void _addSubscriptionTask({
required String id, // required String id,
required ElectrumXSubscription subscription, // required ElectrumXSubscription subscription,
}) { // }) {
_tasks[id] = SocketTask(completer: null, subscription: subscription); // _tasks[id] = SocketTask(completer: null, subscription: subscription);
} // }
//
Future<dynamic> _call({ // Future<dynamic> _call({
required String method, // required String method,
List<dynamic> params = const [], // List<dynamic> params = const [],
}) async { // }) async {
final completer = Completer<dynamic>(); // final completer = Completer<dynamic>();
_currentRequestID++; // _currentRequestID++;
final id = _currentRequestID.toString(); // final id = _currentRequestID.toString();
_addTask(id: id, completer: completer); // _addTask(id: id, completer: completer);
//
_socket?.write( // _socket?.write(
_buildJsonRequestString( // _buildJsonRequestString(
method: method, // method: method,
id: id, // id: id,
params: params, // params: params,
), // ),
); // );
//
return completer.future; // return completer.future;
} // }
//
Future<dynamic> _callWithTimeout({ // Future<dynamic> _callWithTimeout({
required String method, // required String method,
List<dynamic> params = const [], // List<dynamic> params = const [],
Duration timeout = const Duration(seconds: 2), // Duration timeout = const Duration(seconds: 2),
}) async { // }) async {
final completer = Completer<dynamic>(); // final completer = Completer<dynamic>();
_currentRequestID++; // _currentRequestID++;
final id = _currentRequestID.toString(); // final id = _currentRequestID.toString();
_addTask(id: id, completer: completer); // _addTask(id: id, completer: completer);
//
_socket?.write( // _socket?.write(
_buildJsonRequestString( // _buildJsonRequestString(
method: method, // method: method,
id: id, // id: id,
params: params, // params: params,
), // ),
); // );
//
Timer(timeout, () { // Timer(timeout, () {
if (!completer.isCompleted) { // if (!completer.isCompleted) {
completer.completeError( // completer.completeError(
Exception("Request \"id: $id, method: $method\" timed out!"), // Exception("Request \"id: $id, method: $method\" timed out!"),
); // );
} // }
}); // });
//
return completer.future; // return completer.future;
} // }
//
ElectrumXSubscription _subscribe({ // ElectrumXSubscription _subscribe({
required String taskId, // required String taskId,
required String method, // required String method,
List<dynamic> params = const [], // List<dynamic> params = const [],
}) { // }) {
// try { // // try {
final subscription = ElectrumXSubscription(); // final subscription = ElectrumXSubscription();
_addSubscriptionTask(id: taskId, subscription: subscription); // _addSubscriptionTask(id: taskId, subscription: subscription);
_currentRequestID++; // _currentRequestID++;
_socket?.write( // _socket?.write(
_buildJsonRequestString( // _buildJsonRequestString(
method: method, // method: method,
id: taskId, // id: taskId,
params: params, // params: params,
), // ),
); // );
//
return subscription; // return subscription;
// } catch (e, s) { // // } catch (e, s) {
// Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error); // // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
// return null; // // return null;
// // }
// }
//
// /// Ping the server to ensure it is responding
// ///
// /// Returns true if ping succeeded
// Future<bool> 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: [],
// );
// }
// } // }
}
/// Ping the server to ensure it is responding
///
/// Returns true if ping succeeded
Future<bool> 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: [],
);
}
}

View file

@ -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;
}

View file

@ -16,7 +16,6 @@ import 'package:cw_core/node.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_libmonero/monero/monero.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/node_service.dart';
import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/services/notifications_service.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/services/trade_service.dart';
import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/themes/theme_service.dart'; import 'package:stackwallet/themes/theme_service.dart';
@ -97,7 +97,7 @@ void main() async {
setWindowMaxSize(Size.infinite); setWindowMaxSize(Size.infinite);
final screenHeight = screen?.frame.height; 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 // starting to height be 3/4 screen height or 900, whichever is smaller
final height = min<double>(screenHeight * 0.75, 900); final height = min<double>(screenHeight * 0.75, 900);
setWindowFrame( setWindowFrame(
@ -167,6 +167,16 @@ void main() async {
await Hive.openBox<dynamic>(DB.boxNamePrefs); await Hive.openBox<dynamic>(DB.boxNamePrefs);
await Prefs.instance.init(); 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(); await StackFileSystem.initThemesDir();
// Desktop migrate handled elsewhere (currently desktop_login_view.dart) // Desktop migrate handled elsewhere (currently desktop_login_view.dart)

124
lib/networking/http.dart Normal file
View file

@ -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<int> bodyBytes;
String get body => utf8.decode(bodyBytes, allowMalformed: true);
Response(this.bodyBytes, this.code);
}
class HTTP {
Future<Response> get({
required Uri url,
Map<String, String>? 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<Response> post({
required Uri url,
Map<String, String>? 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<Uint8List> _bodyBytes(HttpClientResponse response) {
final completer = Completer<Uint8List>();
final List<int> bytes = [];
response.listen(
(data) {
bytes.addAll(data);
},
onDone: () => completer.complete(
Uint8List.fromList(bytes),
),
);
return completer.future;
}
}

View file

@ -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<List<int>> _responseController =
StreamController.broadcast();
/// A StreamController that listens to the _secureSocksSocket and broadcasts.
final StreamController<List<int>> _secureResponseController =
StreamController.broadcast();
/// Getter for the StreamController that listens to the _socksSocket and
/// broadcasts, or the _secureSocksSocket and broadcasts if SSL is enabled.
StreamController<List<int>> get responseController =>
sslEnabled ? _secureResponseController : _responseController;
/// A StreamSubscription that listens to the _socksSocket or the
/// _secureSocksSocket if SSL is enabled.
StreamSubscription<List<int>>? _subscription;
/// Getter for the StreamSubscription that listens to the _socksSocket or the
/// _secureSocksSocket if SSL is enabled.
StreamSubscription<List<int>>? 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<SOCKSSocket> 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<void> _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<void> 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<void> 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<int> 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<void> 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<void> 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<List<int>> listen(
void Function(List<int> 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,
);
}
}

View file

@ -8,7 +8,10 @@
* *
*/ */
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.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/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/themes/stack_colors.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/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
@ -30,16 +34,17 @@ class AddTokenListElementData {
bool selected = false; bool selected = false;
} }
class AddTokenListElement extends StatefulWidget { class AddTokenListElement extends ConsumerStatefulWidget {
const AddTokenListElement({Key? key, required this.data}) : super(key: key); const AddTokenListElement({Key? key, required this.data}) : super(key: key);
final AddTokenListElementData data; final AddTokenListElementData data;
@override @override
State<AddTokenListElement> createState() => _AddTokenListElementState(); ConsumerState<AddTokenListElement> createState() =>
_AddTokenListElementState();
} }
class _AddTokenListElementState extends State<AddTokenListElement> { class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
final bool isDesktop = Util.isDesktop; final bool isDesktop = Util.isDesktop;
@override @override
@ -74,6 +79,17 @@ class _AddTokenListElementState extends State<AddTokenListElement> {
currency.image, currency.image,
width: iconSize, width: iconSize,
height: iconSize, height: iconSize,
placeholderBuilder: (_) => SvgPicture.file(
File(
ref.watch(
themeAssetsProvider.select(
(value) => value.stackIcon,
),
),
),
width: iconSize,
height: iconSize,
),
) )
: SvgPicture.asset( : SvgPicture.asset(
widget.data.token.symbol == "BNB" widget.data.token.symbol == "BNB"

View file

@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.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/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/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/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'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@ -336,7 +337,11 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
ref.read(walletsServiceChangeNotifierProvider); ref.read(walletsServiceChangeNotifierProvider);
final name = textEditingController.text; final name = textEditingController.text;
if (await walletsService.checkForDuplicate(name)) { final hasDuplicateName =
await walletsService.checkForDuplicate(name);
if (mounted) {
if (hasDuplicateName) {
unawaited(showFloatingFlushBar( unawaited(showFloatingFlushBar(
type: FlushBarType.warning, type: FlushBarType.warning,
message: "Wallet name already in use.", message: "Wallet name already in use.",
@ -352,34 +357,44 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
} }
if (mounted) { if (mounted) {
ref
.read(mnemonicWordCountStateProvider.state)
.state =
Constants.possibleLengthsForCoin(coin).last;
ref.read(pNewWalletOptions.notifier).state = null;
switch (widget.addWalletType) { switch (widget.addWalletType) {
case AddWalletType.New: case AddWalletType.New:
unawaited(Navigator.of(context).pushNamed( unawaited(
NewWalletRecoveryPhraseWarningView.routeName, Navigator.of(context).pushNamed(
coin.hasMnemonicPassphraseSupport
? NewWalletOptionsView.routeName
: NewWalletRecoveryPhraseWarningView
.routeName,
arguments: Tuple2( arguments: Tuple2(
name, name,
coin, coin,
), ),
)); ),
);
break; break;
case AddWalletType.Restore: case AddWalletType.Restore:
ref unawaited(
.read(mnemonicWordCountStateProvider.state) Navigator.of(context).pushNamed(
.state = Constants.possibleLengthsForCoin(
coin)
.first;
unawaited(Navigator.of(context).pushNamed(
RestoreOptionsView.routeName, RestoreOptionsView.routeName,
arguments: Tuple2( arguments: Tuple2(
name, name,
coin, coin,
), ),
)); ),
);
break; break;
} }
} }
} }
} }
}
: null, : null,
style: _nextEnabled style: _nextEnabled
? Theme.of(context) ? Theme.of(context)

View file

@ -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<NewWalletOptionsView> createState() =>
_NewWalletOptionsViewState();
}
class _NewWalletOptionsViewState extends ConsumerState<NewWalletOptionsView> {
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<StackColors>()!.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<StackColors>()!.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<NewWalletOptions>(
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<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.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<int>(
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<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
if (!Util.isDesktop)
MobileMnemonicLengthSelector(
chooseMnemonicLength: () {
showModalBottomSheet<dynamic>(
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<StackColors>()!
.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<StackColors>()!
.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,
),
],
),
),
);
}
}

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.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_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/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'; 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:stackwallet/widgets/rounded_white_container.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class NewWalletRecoveryPhraseWarningView extends StatefulWidget { class NewWalletRecoveryPhraseWarningView extends ConsumerStatefulWidget {
const NewWalletRecoveryPhraseWarningView({ const NewWalletRecoveryPhraseWarningView({
Key? key, Key? key,
required this.coin, required this.coin,
@ -51,12 +52,12 @@ class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
final String walletName; final String walletName;
@override @override
State<NewWalletRecoveryPhraseWarningView> createState() => ConsumerState<NewWalletRecoveryPhraseWarningView> createState() =>
_NewWalletRecoveryPhraseWarningViewState(); _NewWalletRecoveryPhraseWarningViewState();
} }
class _NewWalletRecoveryPhraseWarningViewState class _NewWalletRecoveryPhraseWarningViewState
extends State<NewWalletRecoveryPhraseWarningView> { extends ConsumerState<NewWalletRecoveryPhraseWarningView> {
late final Coin coin; late final Coin coin;
late final String walletName; late final String walletName;
late final bool isDesktop; late final bool isDesktop;
@ -72,6 +73,10 @@ class _NewWalletRecoveryPhraseWarningViewState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
final options = ref.read(pNewWalletOptions.state).state;
final seedCount = options?.mnemonicWordsCount ??
Constants.defaultSeedPhraseLengthFor(coin: coin);
return MasterScaffold( return MasterScaffold(
isDesktop: isDesktop, isDesktop: isDesktop,
@ -172,7 +177,7 @@ class _NewWalletRecoveryPhraseWarningViewState
child: isDesktop child: isDesktop
? Text( ? Text(
"On the next screen you will see " "On the next screen you will see "
"${Constants.defaultSeedPhraseLengthFor(coin: coin)} " "$seedCount "
"words that make up your recovery phrase.\n\nPlease " "words that make up your recovery phrase.\n\nPlease "
"write it down. Keep it safe and never share it with " "write it down. Keep it safe and never share it with "
"anyone. Your recovery phrase is the only way you can" "anyone. Your recovery phrase is the only way you can"
@ -216,9 +221,7 @@ class _NewWalletRecoveryPhraseWarningViewState
), ),
), ),
TextSpan( TextSpan(
text: text: "$seedCount words",
"${Constants.defaultSeedPhraseLengthFor(coin: coin)}"
" words",
style: STextStyles.desktopH3(context).copyWith( style: STextStyles.desktopH3(context).copyWith(
color: Theme.of(context) color: Theme.of(context)
.extension<StackColors>()! .extension<StackColors>()!
@ -496,7 +499,24 @@ class _NewWalletRecoveryPhraseWarningViewState
final manager = Manager(wallet); 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 // pop progress dialog
if (mounted) { if (mounted) {

View file

@ -535,7 +535,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
enableSuggestions: false, enableSuggestions: false,
autocorrect: false, autocorrect: false,
decoration: standardInputDecoration( decoration: standardInputDecoration(
"Recovery phrase password", "BIP39 passphrase",
passwordFocusNode, passwordFocusNode,
context, context,
).copyWith( ).copyWith(
@ -586,7 +586,9 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
RoundedWhiteContainer( RoundedWhiteContainer(
child: Center( child: Center(
child: Text( 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 style: isDesktop
? STextStyles.desktopTextExtraSmall(context) ? STextStyles.desktopTextExtraSmall(context)
.copyWith( .copyWith(

View file

@ -98,7 +98,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
final List<TextEditingController> _controllers = []; final List<TextEditingController> _controllers = [];
final List<FormInputStatus> _inputStatuses = []; final List<FormInputStatus> _inputStatuses = [];
final List<FocusNode> _focusNodes = []; // final List<FocusNode> _focusNodes = [];
late final BarcodeScannerInterface scanner; late final BarcodeScannerInterface scanner;
@ -152,7 +152,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
for (int i = 0; i < _seedWordCount; i++) { for (int i = 0; i < _seedWordCount; i++) {
_controllers.add(TextEditingController()); _controllers.add(TextEditingController());
_inputStatuses.add(FormInputStatus.empty); _inputStatuses.add(FormInputStatus.empty);
_focusNodes.add(FocusNode()); // _focusNodes.add(FocusNode());
} }
super.initState(); super.initState();
@ -821,8 +821,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
i * 4 + j - 1 == 1 i * 4 + j - 1 == 1
? textSelectionControls ? textSelectionControls
: null, : null,
focusNode: // focusNode:
_focusNodes[i * 4 + j - 1], // _focusNodes[i * 4 + j - 1],
onChanged: (value) { onChanged: (value) {
final FormInputStatus final FormInputStatus
formInputStatus; formInputStatus;
@ -841,18 +841,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid; FormInputStatus.invalid;
} }
if (formInputStatus == // if (formInputStatus ==
FormInputStatus.valid) { // FormInputStatus.valid) {
if (i * 4 + j < // if (i * 4 + j <
_focusNodes.length) { // _focusNodes.length) {
_focusNodes[i * 4 + j] // _focusNodes[i * 4 + j]
.requestFocus(); // .requestFocus();
} else if (i * 4 + j == // } else if (i * 4 + j ==
_focusNodes.length) { // _focusNodes.length) {
_focusNodes[i * 4 + j - 1] // _focusNodes[i * 4 + j - 1]
.unfocus(); // .unfocus();
} // }
} // }
setState(() { setState(() {
_inputStatuses[i * 4 + _inputStatuses[i * 4 +
j - j -
@ -929,7 +929,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
selectionControls: i == 1 selectionControls: i == 1
? textSelectionControls ? textSelectionControls
: null, : null,
focusNode: _focusNodes[i], // focusNode: _focusNodes[i],
onChanged: (value) { onChanged: (value) {
final FormInputStatus final FormInputStatus
formInputStatus; formInputStatus;
@ -948,27 +948,27 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid; FormInputStatus.invalid;
} }
if (formInputStatus == // if (formInputStatus ==
FormInputStatus // FormInputStatus
.valid && // .valid &&
(i - 1) < // (i - 1) <
_focusNodes.length) { // _focusNodes.length) {
Focus.of(context) // Focus.of(context)
.requestFocus( // .requestFocus(
_focusNodes[i]); // _focusNodes[i]);
} // }
if (formInputStatus == // if (formInputStatus ==
FormInputStatus.valid) { // FormInputStatus.valid) {
if (i + 1 < // if (i + 1 <
_focusNodes.length) { // _focusNodes.length) {
_focusNodes[i + 1] // _focusNodes[i + 1]
.requestFocus(); // .requestFocus();
} else if (i + 1 == // } else if (i + 1 ==
_focusNodes.length) { // _focusNodes.length) {
_focusNodes[i].unfocus(); // _focusNodes[i].unfocus();
} // }
} // }
}, },
controller: _controllers[i], controller: _controllers[i],
style: style:
@ -1068,7 +1068,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
AutovalidateMode.onUserInteraction, AutovalidateMode.onUserInteraction,
selectionControls: selectionControls:
i == 1 ? textSelectionControls : null, i == 1 ? textSelectionControls : null,
focusNode: _focusNodes[i - 1], // focusNode: _focusNodes[i - 1],
onChanged: (value) { onChanged: (value) {
final FormInputStatus formInputStatus; final FormInputStatus formInputStatus;
@ -1084,14 +1084,14 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
FormInputStatus.invalid; FormInputStatus.invalid;
} }
if (formInputStatus == // if (formInputStatus ==
FormInputStatus.valid) { // FormInputStatus.valid) {
if (i < _focusNodes.length) { // if (i < _focusNodes.length) {
_focusNodes[i].requestFocus(); // _focusNodes[i].requestFocus();
} else if (i == _focusNodes.length) { // } else if (i == _focusNodes.length) {
_focusNodes[i - 1].unfocus(); // _focusNodes[i - 1].unfocus();
} // }
} // }
setState(() { setState(() {
_inputStatuses[i - 1] = _inputStatuses[i - 1] =
formInputStatus; formInputStatus;

View file

@ -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<VerifyMnemonicPassphraseDialog> createState() =>
_VerifyMnemonicPassphraseDialogState();
}
class _VerifyMnemonicPassphraseDialogState
extends ConsumerState<VerifyMnemonicPassphraseDialog> {
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<StackColors>()!
.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,
),
),
],
),
),
);
}
}

View file

@ -16,9 +16,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/notifications/show_flush_bar.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/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/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/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/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/home_view/home_view.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_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'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
@ -98,8 +100,25 @@ class _VerifyRecoveryPhraseViewState
// } // }
// } // }
Future<bool> _verifyMnemonicPassphrase() async {
final result = await showDialog<String?>(
context: context,
builder: (_) => const VerifyMnemonicPassphraseDialog(),
);
return result == "verified";
}
Future<void> _continue(bool isMatch) async { Future<void> _continue(bool isMatch) async {
if (isMatch) { if (isMatch) {
if (ref.read(pNewWalletOptions.state).state != null) {
final passphraseVerified = await _verifyMnemonicPassphrase();
if (!passphraseVerified) {
return;
}
}
await ref.read(walletsServiceChangeNotifierProvider).setMnemonicVerified( await ref.read(walletsServiceChangeNotifierProvider).setMnemonicVerified(
walletId: _manager.walletId, walletId: _manager.walletId,
); );

View file

@ -9,27 +9,56 @@
*/ */
import 'package:flutter/material.dart'; 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/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/pages/buy_view/buy_form.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/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
class BuyView extends StatelessWidget { class BuyView extends ConsumerStatefulWidget {
const BuyView({ const BuyView({
Key? key, Key? key,
this.coin, this.coin,
this.tokenContract, this.tokenContract,
}) : super(key: key); }) : super(key: key);
static const String routeName = "/stackBuyView";
final Coin? coin; final Coin? coin;
final EthContract? tokenContract; final EthContract? tokenContract;
static const String routeName = "/stackBuyView";
@override
ConsumerState<BuyView> createState() => _BuyViewState();
}
class _BuyViewState extends ConsumerState<BuyView> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType"); debugPrint("BUILD: $runtimeType");
return SafeArea( return Stack(
children: [
SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,
@ -41,6 +70,21 @@ class BuyView extends StatelessWidget {
tokenContract: tokenContract, tokenContract: tokenContract,
), ),
), ),
),
if (torEnabled)
Container(
color: Theme.of(context)
.extension<StackColors>()!
.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",
),
),
],
); );
} }
} }

View file

@ -24,6 +24,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.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/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart';
import '../../../services/exchange/exchange.dart';
class ExchangeCurrencySelectionView extends StatefulWidget { class ExchangeCurrencySelectionView extends StatefulWidget {
const ExchangeCurrencySelectionView({ const ExchangeCurrencySelectionView({
Key? key, Key? key,
@ -125,7 +128,7 @@ class _ExchangeCurrencySelectionViewState
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (context) => StackDialog( builder: (context) => StackDialog(
title: "ChangeNOW Error", title: "Exchange Error",
message: "Failed to load currency data: ${cn.exception}", message: "Failed to load currency data: ${cn.exception}",
leftButton: SecondaryButton( leftButton: SecondaryButton(
label: "Ok", label: "Ok",
@ -169,6 +172,15 @@ class _ExchangeCurrencySelectionViewState
.thenByName() .thenByName()
.findAll(); .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); return _getDistinctCurrenciesFrom(currencies);
} }

View file

@ -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/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.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_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/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount_unit.dart'; import 'package:stackwallet/utilities/amount/amount_unit.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/exchange_rate_type_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/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/conditional_parent.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:tuple/tuple.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../services/exchange/exchange_response.dart';
class ExchangeForm extends ConsumerStatefulWidget { class ExchangeForm extends ConsumerStatefulWidget {
const ExchangeForm({ const ExchangeForm({
Key? key, Key? key,
@ -78,7 +80,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
late final Coin? coin; late final Coin? coin;
late final bool walletInitiated; late final bool walletInitiated;
final exchanges = [ var exchanges = [
MajesticBankExchange.instance, MajesticBankExchange.instance,
ChangeNowExchange.instance, ChangeNowExchange.instance,
TrocadorExchange.instance, TrocadorExchange.instance,
@ -773,6 +775,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
}); });
} }
// Instantiate the Tor service.
torService = TorService.sharedInstance;
// Filter exchanges based on Tor support.
if (Prefs.instance.useTor) {
exchanges = Exchange.exchangesWithTorSupport;
}
super.initState(); super.initState();
} }
@ -1007,4 +1017,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
], ],
); );
} }
// TorService instance.
late TorService torService;
} }

View file

@ -252,10 +252,17 @@ class _Step4ViewState extends ConsumerState<Step4View> {
}, },
); );
} else { } else {
final memo =
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
? model.trade!.payInExtraId.isNotEmpty
? model.trade!.payInExtraId
: null
: null;
txDataFuture = manager.prepareSend( txDataFuture = manager.prepareSend(
address: address, address: address,
amount: amount, amount: amount,
args: { args: {
"memo": memo,
"feeRate": FeeRateType.average, "feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider) // ref.read(feeRateTypeStateProvider)
}, },
@ -568,6 +575,74 @@ class _Step4ViewState extends ConsumerState<Step4View> {
const SizedBox( const SizedBox(
height: 6, 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<StackColors>()!
.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( RoundedWhiteContainer(
child: Row( child: Row(
children: [ children: [

View file

@ -268,10 +268,17 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
// if not firo then do normal send // if not firo then do normal send
if (shouldSendPublicFiroFunds == null) { if (shouldSendPublicFiroFunds == null) {
final memo =
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
? trade.payInExtraId.isNotEmpty
? trade.payInExtraId
: null
: null;
txDataFuture = manager.prepareSend( txDataFuture = manager.prepareSend(
address: address, address: address,
amount: amount, amount: amount,
args: { args: {
"memo": memo,
"feeRate": FeeRateType.average, "feeRate": FeeRateType.average,
// ref.read(feeRateTypeStateProvider) // ref.read(feeRateTypeStateProvider)
}, },

View file

@ -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/pages/exchange_view/sub_widgets/exchange_provider_option.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.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/majestic_bank/majestic_bank_exchange.dart';
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
@ -44,6 +46,13 @@ class _ExchangeProviderOptionsState
required AggregateCurrency? sendCurrency, required AggregateCurrency? sendCurrency,
required AggregateCurrency? receiveCurrency, 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); final send = sendCurrency?.forExchange(exchangeName);
if (send == null) return false; if (send == null) return false;

View file

@ -850,6 +850,81 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
: const SizedBox( : const SizedBox(
height: 12, 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<StackColors>()!
.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( RoundedWhiteContainer(
padding: isDesktop padding: isDesktop
? const EdgeInsets.all(16) ? const EdgeInsets.all(16)

View file

@ -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/global/notifications_provider.dart';
import 'package:stackwallet/providers/ui/home_view_index_provider.dart'; import 'package:stackwallet/providers/ui/home_view_index_provider.dart';
import 'package:stackwallet/providers/ui/unread_notifications_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/stack_colors.dart';
import 'package:stackwallet/themes/theme_providers.dart'; import 'package:stackwallet/themes/theme_providers.dart';
import 'package:stackwallet/utilities/assets.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/animated_widgets/rotate_icon.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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'; import 'package:stackwallet/widgets/stack_dialog.dart';
class HomeView extends ConsumerStatefulWidget { class HomeView extends ConsumerStatefulWidget {
@ -55,6 +57,8 @@ class _HomeViewState extends ConsumerState<HomeView> {
bool _exitEnabled = false; bool _exitEnabled = false;
late TorConnectionStatus _currentSyncStatus;
// final _buyDataLoadingService = BuyDataLoadingService(); // final _buyDataLoadingService = BuyDataLoadingService();
Future<bool> _onWillPop() async { Future<bool> _onWillPop() async {
@ -125,6 +129,20 @@ class _HomeViewState extends ConsumerState<HomeView> {
ref.read(notificationsProvider).startCheckingWatchedNotifications(); 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(); super.initState();
} }
@ -200,6 +218,17 @@ class _HomeViewState extends ConsumerState<HomeView> {
], ],
), ),
actions: [ actions: [
const Padding(
padding: EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: SmallTorIcon(),
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 10, top: 10,

View file

@ -5,20 +5,22 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.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/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
@ -230,11 +232,16 @@ class _OrdinalImageGroup extends StatelessWidget {
static const _spacing = 12.0; static const _spacing = 12.0;
Future<String> _savePngToFile() async { Future<String> _savePngToFile() async {
final response = await get(Uri.parse(ordinal.content)); HTTP client = HTTP();
if (response.statusCode != 200) { final response = await client.get(
throw Exception( url: Uri.parse(ordinal.content),
"statusCode=${response.statusCode} body=${response.bodyBytes}"); proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.code != 200) {
throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
} }
final bytes = response.bodyBytes; final bytes = response.bodyBytes;

View file

@ -307,14 +307,20 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
if (coin != Coin.epicCash && if (coin != Coin.epicCash &&
coin != Coin.ethereum && coin != Coin.ethereum &&
coin != Coin.banano && coin != Coin.banano &&
coin != Coin.nano) coin != Coin.nano &&
coin != Coin.stellar &&
coin != Coin.stellarTestnet &&
coin != Coin.tezos)
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
if (coin != Coin.epicCash && if (coin != Coin.epicCash &&
coin != Coin.ethereum && coin != Coin.ethereum &&
coin != Coin.banano && coin != Coin.banano &&
coin != Coin.nano) coin != Coin.nano &&
coin != Coin.stellar &&
coin != Coin.stellarTestnet &&
coin != Coin.tezos)
TextButton( TextButton(
onPressed: generateNewAddress, onPressed: generateNewAddress,
style: Theme.of(context) style: Theme.of(context)

View file

@ -103,6 +103,7 @@ class _SendViewState extends ConsumerState<SendView> {
late TextEditingController noteController; late TextEditingController noteController;
late TextEditingController onChainNoteController; late TextEditingController onChainNoteController;
late TextEditingController feeController; late TextEditingController feeController;
late TextEditingController memoController;
late final SendViewAutoFillData? _data; late final SendViewAutoFillData? _data;
@ -111,6 +112,9 @@ class _SendViewState extends ConsumerState<SendView> {
final _onChainNoteFocusNode = FocusNode(); final _onChainNoteFocusNode = FocusNode();
final _cryptoFocus = FocusNode(); final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode(); final _baseFocus = FocusNode();
final _memoFocus = FocusNode();
late final bool isStellar;
Amount? _amountToSend; Amount? _amountToSend;
Amount? _cachedAmountToSend; Amount? _cachedAmountToSend;
@ -522,10 +526,15 @@ class _SendViewState extends ConsumerState<SendView> {
}, },
); );
} else { } else {
final memo =
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
? memoController.text
: null;
txDataFuture = manager.prepareSend( txDataFuture = manager.prepareSend(
address: _address!, address: _address!,
amount: amount, amount: amount,
args: { args: {
"memo": memo,
"feeRate": ref.read(feeRateTypeStateProvider), "feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null, "satsPerVByte": isCustomFee ? customFeeRate : null,
"UTXOs": (manager.hasCoinControlSupport && "UTXOs": (manager.hasCoinControlSupport &&
@ -622,6 +631,7 @@ class _SendViewState extends ConsumerState<SendView> {
walletId = widget.walletId; walletId = widget.walletId;
clipboard = widget.clipboard; clipboard = widget.clipboard;
scanner = widget.barcodeScanner; scanner = widget.barcodeScanner;
isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
sendToController = TextEditingController(); sendToController = TextEditingController();
cryptoAmountController = TextEditingController(); cryptoAmountController = TextEditingController();
@ -629,6 +639,7 @@ class _SendViewState extends ConsumerState<SendView> {
noteController = TextEditingController(); noteController = TextEditingController();
onChainNoteController = TextEditingController(); onChainNoteController = TextEditingController();
feeController = TextEditingController(); feeController = TextEditingController();
memoController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged; onCryptoAmountChanged = _cryptoAmountChanged;
cryptoAmountController.addListener(onCryptoAmountChanged); cryptoAmountController.addListener(onCryptoAmountChanged);
@ -704,12 +715,14 @@ class _SendViewState extends ConsumerState<SendView> {
noteController.dispose(); noteController.dispose();
onChainNoteController.dispose(); onChainNoteController.dispose();
feeController.dispose(); feeController.dispose();
memoController.dispose();
_noteFocusNode.dispose(); _noteFocusNode.dispose();
_onChainNoteFocusNode.dispose(); _onChainNoteFocusNode.dispose();
_addressFocusNode.dispose(); _addressFocusNode.dispose();
_cryptoFocus.dispose(); _cryptoFocus.dispose();
_baseFocus.dispose(); _baseFocus.dispose();
_memoFocus.dispose();
super.dispose(); super.dispose();
} }
@ -1298,6 +1311,88 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
), ),
), ),
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(
builder: (_) { builder: (_) {
final error = _updateInvalidAddressText( final error = _updateInvalidAddressText(
@ -1817,7 +1912,8 @@ class _SendViewState extends ConsumerState<SendView> {
), ),
child: TextField( child: TextField(
autocorrect: Util.isDesktop ? false : true, autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true, enableSuggestions:
Util.isDesktop ? false : true,
maxLength: 256, maxLength: 256,
controller: onChainNoteController, controller: onChainNoteController,
focusNode: _onChainNoteFocusNode, focusNode: _onChainNoteFocusNode,
@ -1828,7 +1924,8 @@ class _SendViewState extends ConsumerState<SendView> {
_onChainNoteFocusNode, _onChainNoteFocusNode,
context, context,
).copyWith( ).copyWith(
suffixIcon: onChainNoteController.text.isNotEmpty suffixIcon: onChainNoteController
.text.isNotEmpty
? Padding( ? Padding(
padding: padding:
const EdgeInsets.only(right: 0), const EdgeInsets.only(right: 0),
@ -1839,7 +1936,8 @@ class _SendViewState extends ConsumerState<SendView> {
child: const XIcon(), child: const XIcon(),
onTap: () async { onTap: () async {
setState(() { setState(() {
onChainNoteController.text = ""; onChainNoteController
.text = "";
}); });
}, },
), ),
@ -1856,7 +1954,8 @@ class _SendViewState extends ConsumerState<SendView> {
height: 12, height: 12,
), ),
Text( Text(
(coin == Coin.epicCash) ? "Local Note (optional)" (coin == Coin.epicCash)
? "Local Note (optional)"
: "Note (optional)", : "Note (optional)",
style: STextStyles.smallMed12(context), style: STextStyles.smallMed12(context),
textAlign: TextAlign.left, textAlign: TextAlign.left,

View file

@ -15,11 +15,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS; import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS; import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart';
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
import 'package:package_info_plus/package_info_plus.dart'; 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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@ -39,14 +41,17 @@ Future<bool> doesCommitExist(
String commit, String commit,
) async { ) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info); Logging.instance.log("doesCommitExist", level: LogLevel.Info);
final Client client = Client(); // final Client client = Client();
HTTP client = HTTP();
try { try {
final uri = Uri.parse( final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit"); "$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
final commitQuery = await client.get( final commitQuery = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final response = jsonDecode(commitQuery.body.toString()); final response = jsonDecode(commitQuery.body.toString());
@ -76,14 +81,16 @@ Future<bool> isHeadCommit(
String commit, String commit,
) async { ) async {
Logging.instance.log("doesCommitExist", level: LogLevel.Info); Logging.instance.log("doesCommitExist", level: LogLevel.Info);
final Client client = Client(); HTTP client = HTTP();
try { try {
final uri = Uri.parse( final uri = Uri.parse(
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch"); "$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
final commitQuery = await client.get( final commitQuery = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final response = jsonDecode(commitQuery.body.toString()); final response = jsonDecode(commitQuery.body.toString());

View file

@ -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/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/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/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/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
@ -159,6 +160,18 @@ class GlobalSettingsView extends StatelessWidget {
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
SettingsListButton(
iconAssetName: Assets.svg.tor,
iconSize: 18,
title: "Tor Settings",
onPressed: () {
Navigator.of(context)
.pushNamed(TorSettingsView.routeName);
},
),
const SizedBox(
height: 8,
),
SettingsListButton( SettingsListButton(
iconAssetName: Assets.svg.node, iconAssetName: Assets.svg.node,
iconSize: 16, iconSize: 16,

View file

@ -167,77 +167,77 @@ class HiddenSettings extends StatelessWidget {
// ), // ),
// ); // );
// }), // }),
const SizedBox( // const SizedBox(
height: 12, // height: 12,
), // ),
Consumer(builder: (_, ref, __) { // Consumer(builder: (_, ref, __) {
return GestureDetector( // return GestureDetector(
onTap: () async { // onTap: () async {
ref // ref
.read(priceAnd24hChangeNotifierProvider) // .read(priceAnd24hChangeNotifierProvider)
.tokenContractAddressesToCheck // .tokenContractAddressesToCheck
.add( // .add(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
ref // ref
.read(priceAnd24hChangeNotifierProvider) // .read(priceAnd24hChangeNotifierProvider)
.tokenContractAddressesToCheck // .tokenContractAddressesToCheck
.add( // .add(
"0xdAC17F958D2ee523a2206206994597C13D831ec7"); // "0xdAC17F958D2ee523a2206206994597C13D831ec7");
await ref // await ref
.read(priceAnd24hChangeNotifierProvider) // .read(priceAnd24hChangeNotifierProvider)
.updatePrice(); // .updatePrice();
//
final x = ref // final x = ref
.read(priceAnd24hChangeNotifierProvider) // .read(priceAnd24hChangeNotifierProvider)
.getTokenPrice( // .getTokenPrice(
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
//
print( // print(
"PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x"); // "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x");
}, // },
child: RoundedWhiteContainer( // child: RoundedWhiteContainer(
child: Text( // child: Text(
"Click me", // "Click me",
style: STextStyles.button(context).copyWith( // style: STextStyles.button(context).copyWith(
color: Theme.of(context) // color: Theme.of(context)
.extension<StackColors>()! // .extension<StackColors>()!
.accentColorDark), // .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(); // const SizedBox(
// // height: 12,
// print(json); // ),
// // Consumer(builder: (_, ref, __) {
// final ee = EthContractInfo.fromJson(json); // return GestureDetector(
// // onTap: () async {
// print(ee); // // final erc20 = Erc20ContractInfo(
}, // // contractAddress: 'some con',
child: RoundedWhiteContainer( // // name: "loonamsn",
child: Text( // // symbol: "DD",
"Click me", // // decimals: 19,
style: STextStyles.button(context).copyWith( // // );
color: Theme.of(context) // //
.extension<StackColors>()! // // final json = erc20.toJson();
.accentColorDark), // //
), // // 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<StackColors>()!
// .accentColorDark),
// ),
// ),
// );
// }),
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),

View file

@ -27,6 +27,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_monero_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/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
@ -193,11 +194,18 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
try { try {
// await client.getSyncStatus(); // await client.getSyncStatus();
} catch (_) {} } catch (_) {}
break;
case Coin.stellar:
case Coin.stellarTestnet:
try {
testPassed =
await testStellarNodeConnection(formData.host!, formData.port!);
} catch (_) {}
break;
case Coin.nano: case Coin.nano:
case Coin.banano: case Coin.banano:
case Coin.stellar: case Coin.tezos:
case Coin.stellarTestnet:
throw UnimplementedError(); throw UnimplementedError();
//TODO: check network/node //TODO: check network/node
} }
@ -730,6 +738,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
case Coin.namecoin: case Coin.namecoin:
case Coin.bitcoincash: case Coin.bitcoincash:
case Coin.particl: case Coin.particl:
case Coin.tezos:
case Coin.bitcoinTestNet: case Coin.bitcoinTestNet:
case Coin.litecoinTestNet: case Coin.litecoinTestNet:
case Coin.bitcoincashTestnet: case Coin.bitcoincashTestnet:

View file

@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart'; import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/utilities/test_eth_node_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_monero_node_connection.dart';
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
@ -172,10 +173,17 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
case Coin.nano: case Coin.nano:
case Coin.banano: case Coin.banano:
case Coin.stellar: case Coin.tezos:
case Coin.stellarTestnet:
throw UnimplementedError(); throw UnimplementedError();
//TODO: check network/node //TODO: check network/node
case Coin.stellar:
case Coin.stellarTestnet:
try {
testPassed = await testStellarNodeConnection(node!.host, node.port);
} catch (_) {
testPassed = false;
}
break;
} }
if (testPassed) { if (testPassed) {

View file

@ -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<TorSettingsView> createState() => _TorSettingsViewState();
}
class _TorSettingsViewState extends ConsumerState<TorSettingsView> {
@override
Widget build(BuildContext context) {
return Background(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor:
Theme.of(context).extension<StackColors>()!.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<dynamic>(
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<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
"Tor killswitch",
style: STextStyles.titleBold12(context),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () {
showDialog<dynamic>(
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<StackColors>()!
.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<TorIcon> createState() => _TorIconState();
}
class _TorIconState extends ConsumerState<TorIcon> {
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<void> 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<StackColors>()!,
),
width: 200,
height: 200,
),
Text(
_label(
_status,
Theme.of(context).extension<StackColors>()!,
),
style: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.popupBG,
),
),
],
),
),
),
);
}
}
class TorButton extends ConsumerStatefulWidget {
const TorButton({super.key});
@override
ConsumerState<TorButton> createState() => _TorButtonState();
}
class _TorButtonState extends ConsumerState<TorButton> {
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<void> 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<StackColors>()!,
),
style: STextStyles.itemSubtitle(context).copyWith(
color: _color(
_status,
Theme.of(context).extension<StackColors>()!,
),
),
),
],
),
),
),
),
);
}
}
/// 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<void> _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<void> _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
}
}

View file

@ -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/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/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/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/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/animated_text.dart';
@ -92,6 +95,13 @@ class _WalletNetworkSettingsViewState
late int _blocksRemaining; late int _blocksRemaining;
bool _advancedIsExpanded = false; bool _advancedIsExpanded = false;
/// The current status of the Tor connection.
late TorConnectionStatus _torConnectionStatus;
/// The subscription to the TorConnectionStatusChangedEvent.
late final StreamSubscription<TorConnectionStatusChangedEvent>
_torConnectionStatusSubscription;
Future<void> _attemptRescan() async { Future<void> _attemptRescan() async {
if (!Platform.isLinux) await Wakelock.enable(); 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<TorConnectionStatusChangedEvent>().listen(
(event) async {
// Rebuild the widget.
setState(() {
_torConnectionStatus = event.newStatus;
});
// TODO implement spinner or animations and control from here
},
);
super.initState(); super.initState();
} }
@ -277,6 +306,7 @@ class _WalletNetworkSettingsViewState
_syncStatusSubscription.cancel(); _syncStatusSubscription.cancel();
_refreshSubscription.cancel(); _refreshSubscription.cancel();
_blocksRemainingSubscription?.cancel(); _blocksRemainingSubscription?.cancel();
_torConnectionStatusSubscription.cancel();
super.dispose(); super.dispose();
} }
@ -340,6 +370,11 @@ class _WalletNetworkSettingsViewState
style: STextStyles.navBarTitle(context), style: STextStyles.navBarTitle(context),
), ),
actions: [ actions: [
if (ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.coin !=
Coin.epicCash)
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 10, top: 10,
@ -381,7 +416,8 @@ class _WalletNetworkSettingsViewState
.extension<StackColors>()! .extension<StackColors>()!
.popupBG, .popupBG,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius), Constants
.size.circularBorderRadius),
// boxShadow: [CFColors.standardBoxShadow], // boxShadow: [CFColors.standardBoxShadow],
boxShadow: const [], boxShadow: const [],
), ),
@ -408,8 +444,8 @@ class _WalletNetworkSettingsViewState
color: Colors.transparent, color: Colors.transparent,
child: Text( child: Text(
"Rescan blockchain", "Rescan blockchain",
style: style: STextStyles.baseXS(
STextStyles.baseXS(context), context),
), ),
), ),
), ),
@ -521,14 +557,6 @@ class _WalletNetworkSettingsViewState
"Synchronized", "Synchronized",
style: STextStyles.w600_12(context), style: STextStyles.w600_12(context),
), ),
Text(
"100%",
style: STextStyles.syncPercent(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorGreen,
),
),
], ],
), ),
), ),
@ -749,6 +777,161 @@ class _WalletNetworkSettingsViewState
SizedBox( SizedBox(
height: isDesktop ? 32 : 20, 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<StackColors>()!.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<StackColors>()!
.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<StackColors>()!
.accentColorGreen,
),
),
),
if (!ref.watch(prefsChangeNotifierProvider
.select((value) => value.useTor)))
Container(
width: _iconSize,
height: _iconSize,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.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<StackColors>()!
.textDark,
),
),
),
SizedBox(
width: _boxPadding,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Tor status",
style: STextStyles.desktopTextExtraExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.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( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -786,11 +969,21 @@ class _WalletNetworkSettingsViewState
.select((value) => value.getManager(widget.walletId).coin)), .select((value) => value.getManager(widget.walletId).coin)),
popBackToRoute: WalletNetworkSettingsView.routeName, popBackToRoute: WalletNetworkSettingsView.routeName,
), ),
if (isDesktop) if (isDesktop &&
ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.coin !=
Coin.epicCash)
const SizedBox( const SizedBox(
height: 32, height: 32,
), ),
if (isDesktop) if (isDesktop &&
ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.coin !=
Coin.epicCash)
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 12, bottom: 12,
@ -806,7 +999,12 @@ class _WalletNetworkSettingsViewState
], ],
), ),
), ),
if (isDesktop) if (isDesktop &&
ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.coin !=
Coin.epicCash)
RoundedWhiteContainer( RoundedWhiteContainer(
borderColor: isDesktop borderColor: isDesktop
? Theme.of(context).extension<StackColors>()!.background ? Theme.of(context).extension<StackColors>()!.background

View file

@ -13,7 +13,6 @@ import 'dart:async';
import 'package:event_bus/event_bus.dart'; import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/models/epicbox_config_model.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/address_book_views/address_book_view.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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/text_styles.dart';
import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.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/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_dialog.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
/// [eventBus] should only be set during testing /// [eventBus] should only be set during testing
@ -310,61 +306,6 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
); );
}, },
), ),
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<void>(
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<void>(
[
Future.delayed(
const Duration(
milliseconds: 1500,
),
),
DB.instance
.clearSharedTransactionCache(
coin: coin,
),
],
),
context: context,
message: "Clearing cache...",
);
}
},
);
},
),
if (coin == Coin.nano || coin == Coin.banano) if (coin == Coin.nano || coin == Coin.banano)
const SizedBox( const SizedBox(
height: 8, height: 8,

View file

@ -35,7 +35,6 @@ class TxIcon extends ConsumerWidget {
String _getAssetName( String _getAssetName(
bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) { bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) {
if (!isReceived && transaction.subType == TransactionSubType.mint) { if (!isReceived && transaction.subType == TransactionSubType.mint) {
if (isCancelled) { if (isCancelled) {
return Assets.svg.anonymizeFailed; return Assets.svg.anonymizeFailed;
@ -48,7 +47,7 @@ class TxIcon extends ConsumerWidget {
if (isReceived) { if (isReceived) {
if (isCancelled) { if (isCancelled) {
return assets.receiveCancelled; return assets.receive;
} }
if (isPending) { if (isPending) {
return assets.receivePending; return assets.receivePending;

View file

@ -358,6 +358,8 @@ class _TransactionDetailsViewState
final currentHeight = ref.watch(walletsChangeNotifierProvider final currentHeight = ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).currentHeight)); .select((value) => value.getManager(walletId).currentHeight));
print("THIS TRANSACTION IS $_transaction");
return ConditionalParent( return ConditionalParent(
condition: !isDesktop, condition: !isDesktop,
builder: (child) => Background( builder: (child) => Background(
@ -1577,11 +1579,7 @@ class _TransactionDetailsViewState
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: (coin == Coin.epicCash && floatingActionButton: (coin == Coin.epicCash &&
_transaction.isConfirmed( _transaction.getConfirmations(currentHeight) < 1 &&
currentHeight,
coin.requiredConfirmations,
) ==
false &&
_transaction.isCancelled == false) _transaction.isCancelled == false)
? ConditionalParent( ? ConditionalParent(
condition: isDesktop, condition: isDesktop,

View file

@ -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/custom_loading_overlay.dart';
import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart';
import 'package:stackwallet/widgets/loading_indicator.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/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/buy_nav_icon.dart';
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_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<WalletView> {
], ],
), ),
actions: [ actions: [
const Padding(
padding: EdgeInsets.only(
top: 10,
bottom: 10,
right: 10,
),
child: AspectRatio(
aspectRatio: 1,
child: SmallTorIcon(),
),
),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 10, top: 10,

View file

@ -9,25 +9,43 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/buy_view/buy_form.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/utilities/text_styles.dart';
import 'package:stackwallet/widgets/desktop/desktop_app_bar.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/desktop/desktop_scaffold.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart';
class DesktopBuyView extends StatefulWidget { class DesktopBuyView extends ConsumerStatefulWidget {
const DesktopBuyView({Key? key}) : super(key: key); const DesktopBuyView({Key? key}) : super(key: key);
static const String routeName = "/desktopBuyView"; static const String routeName = "/desktopBuyView";
@override @override
State<DesktopBuyView> createState() => _DesktopBuyViewState(); ConsumerState<DesktopBuyView> createState() => _DesktopBuyViewState();
}
class _DesktopBuyViewState extends ConsumerState<DesktopBuyView> {
late bool torEnabled = false;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {
torEnabled = ref.read(prefsChangeNotifierProvider).useTor;
});
});
super.initState();
} }
class _DesktopBuyViewState extends State<DesktopBuyView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DesktopScaffold( return Stack(
children: [
DesktopScaffold(
appBar: DesktopAppBar( appBar: DesktopAppBar(
isCompactHeight: true, isCompactHeight: true,
leading: Padding( leading: Padding(
@ -79,6 +97,49 @@ class _DesktopBuyViewState extends State<DesktopBuyView> {
], ],
), ),
), ),
),
if (torEnabled)
Container(
color: Theme.of(context)
.extension<StackColors>()!
.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<StackColors>()!
.infoItemLabel,
),
),
],
),
),
),
),
],
); );
} }
} }

View file

@ -155,6 +155,23 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
height: 1, height: 1,
color: Theme.of(context).extension<StackColors>()!.background, color: Theme.of(context).extension<StackColors>()!.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<StackColors>()!.background,
),
DesktopStepItem( DesktopStepItem(
label: "Amount", label: "Amount",
value: value:

View file

@ -15,10 +15,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/pages_desktop_specific/desktop_menu_item.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/providers/desktop/current_desktop_menu_item.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.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'; import 'package:stackwallet/widgets/desktop/living_stack_icon.dart';
enum DesktopMenuItemId { enum DesktopMenuItemId {
@ -52,11 +54,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
final Duration duration = const Duration(milliseconds: 250); final Duration duration = const Duration(milliseconds: 250);
late final List<DMIController> controllers; late final List<DMIController> controllers;
late final DMIController torButtonController;
double _width = expandedWidth; double _width = expandedWidth;
// final _buyDataLoadingService = BuyDataLoadingService();
void updateSelectedMenuItem(DesktopMenuItemId idKey) { void updateSelectedMenuItem(DesktopMenuItemId idKey) {
widget.onSelectionWillChange?.call(idKey); widget.onSelectionWillChange?.call(idKey);
@ -72,6 +73,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
e.toggle?.call(); e.toggle?.call();
} }
torButtonController.toggle?.call();
setState(() { setState(() {
_width = expanded ? minimizedWidth : expandedWidth; _width = expanded ? minimizedWidth : expandedWidth;
}); });
@ -91,6 +94,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
DMIController(), DMIController(),
]; ];
torButtonController = DMIController();
super.initState(); super.initState();
} }
@ -99,6 +104,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
for (var e in controllers) { for (var e in controllers) {
e.dispose(); e.dispose();
} }
torButtonController.dispose();
super.dispose(); super.dispose();
} }
@ -140,7 +147,26 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
), ),
), ),
const SizedBox( 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( Expanded(
child: AnimatedContainer( child: AnimatedContainer(

View file

@ -219,14 +219,20 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
if (coin != Coin.epicCash && if (coin != Coin.epicCash &&
coin != Coin.ethereum && coin != Coin.ethereum &&
coin != Coin.banano && coin != Coin.banano &&
coin != Coin.nano) coin != Coin.nano &&
coin != Coin.stellar &&
coin != Coin.stellarTestnet &&
coin != Coin.tezos)
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
if (coin != Coin.epicCash && if (coin != Coin.epicCash &&
coin != Coin.ethereum && coin != Coin.ethereum &&
coin != Coin.banano && coin != Coin.banano &&
coin != Coin.nano) coin != Coin.nano &&
coin != Coin.stellar &&
coin != Coin.stellarTestnet &&
coin != Coin.tezos)
SecondaryButton( SecondaryButton(
buttonHeight: ButtonHeight.l, buttonHeight: ButtonHeight.l,
onPressed: generateNewAddress, onPressed: generateNewAddress,

View file

@ -97,12 +97,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
late TextEditingController cryptoAmountController; late TextEditingController cryptoAmountController;
late TextEditingController baseAmountController; late TextEditingController baseAmountController;
// late TextEditingController feeController; // late TextEditingController feeController;
late TextEditingController memoController;
late final SendViewAutoFillData? _data; late final SendViewAutoFillData? _data;
final _addressFocusNode = FocusNode(); final _addressFocusNode = FocusNode();
final _cryptoFocus = FocusNode(); final _cryptoFocus = FocusNode();
final _baseFocus = FocusNode(); final _baseFocus = FocusNode();
final _memoFocus = FocusNode();
late final bool isStellar;
String? _note; String? _note;
String? _onChainNote; String? _onChainNote;
@ -326,10 +330,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
}, },
); );
} else { } else {
final memo = isStellar ? memoController.text : null;
txDataFuture = manager.prepareSend( txDataFuture = manager.prepareSend(
address: _address!, address: _address!,
amount: amount, amount: amount,
args: { args: {
"memo": memo,
"feeRate": ref.read(feeRateTypeStateProvider), "feeRate": ref.read(feeRateTypeStateProvider),
"satsPerVByte": isCustomFee ? customFeeRate : null, "satsPerVByte": isCustomFee ? customFeeRate : null,
"UTXOs": (manager.hasCoinControlSupport && "UTXOs": (manager.hasCoinControlSupport &&
@ -663,6 +669,23 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
} }
Future<void> 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) { void fiatTextFieldOnChanged(String baseAmountString) {
final baseAmount = Amount.tryParseFiatString( final baseAmount = Amount.tryParseFiatString(
baseAmountString, baseAmountString,
@ -762,10 +785,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
clipboard = widget.clipboard; clipboard = widget.clipboard;
scanner = widget.barcodeScanner; scanner = widget.barcodeScanner;
isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
sendToController = TextEditingController(); sendToController = TextEditingController();
cryptoAmountController = TextEditingController(); cryptoAmountController = TextEditingController();
baseAmountController = TextEditingController(); baseAmountController = TextEditingController();
memoController = TextEditingController();
// feeController = TextEditingController(); // feeController = TextEditingController();
onCryptoAmountChanged = _cryptoAmountChanged; onCryptoAmountChanged = _cryptoAmountChanged;
@ -814,11 +839,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
sendToController.dispose(); sendToController.dispose();
cryptoAmountController.dispose(); cryptoAmountController.dispose();
baseAmountController.dispose(); baseAmountController.dispose();
memoController.dispose();
// feeController.dispose(); // feeController.dispose();
_addressFocusNode.dispose(); _addressFocusNode.dispose();
_cryptoFocus.dispose(); _cryptoFocus.dispose();
_baseFocus.dispose(); _baseFocus.dispose();
_memoFocus.dispose();
super.dispose(); super.dispose();
} }
@ -1367,6 +1394,67 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
} }
}, },
), ),
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<StackColors>()!
.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) if (!isPaynymSend)
const SizedBox( const SizedBox(
height: 20, height: 20,

View file

@ -3,21 +3,23 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:http/http.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/ordinal.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/notifications/show_flush_bar.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.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/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/wallets_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/themes/stack_colors.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/amount/amount_formatter.dart'; import 'package:stackwallet/utilities/amount/amount_formatter.dart';
import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
@ -50,11 +52,16 @@ class _DesktopOrdinalDetailsViewState
late final UTXO? utxo; late final UTXO? utxo;
Future<String> _savePngToFile() async { Future<String> _savePngToFile() async {
final response = await get(Uri.parse(widget.ordinal.content)); HTTP client = HTTP();
if (response.statusCode != 200) { final response = await client.get(
throw Exception( url: Uri.parse(widget.ordinal.content),
"statusCode=${response.statusCode} body=${response.bodyBytes}"); proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.code != 200) {
throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
} }
final bytes = response.bodyBytes; final bytes = response.bodyBytes;

View file

@ -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/nodes_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_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/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/route_generator.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/text_styles.dart';
@ -56,7 +57,12 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
key: Key("settingsLanguageDesktopKey"), key: Key("settingsLanguageDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute, onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: LanguageOptionSettings.routeName, initialRoute: LanguageOptionSettings.routeName,
), //language ),
const Navigator(
key: Key("settingsTorDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute,
initialRoute: TorSettings.routeName,
), //tor
const Navigator( const Navigator(
key: Key("settingsNodesDesktopKey"), key: Key("settingsNodesDesktopKey"),
onGenerateRoute: RouteGenerator.generateRoute, onGenerateRoute: RouteGenerator.generateRoute,

View file

@ -32,6 +32,7 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> {
"Security", "Security",
"Currency", "Currency",
"Language", "Language",
"Tor settings",
"Nodes", "Nodes",
"Syncing preferences", "Syncing preferences",
"Appearance", "Appearance",

View file

@ -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<TorSettings> createState() => _TorSettingsState();
}
class _TorSettingsState extends ConsumerState<TorSettings> {
/// The global event bus.
EventBus eventBus = GlobalEventBus.instance;
/// Subscription to the TorConnectionStatusChangedEvent.
late StreamSubscription<TorConnectionStatusChangedEvent>
_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<TorConnectionStatusChangedEvent>().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<dynamic>(
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<StackColors>()!
.textDark),
),
TextSpan(
text: "\nWhat is Tor killswitch?",
style: STextStyles.richLink(context).copyWith(
fontSize: 14,
),
recognizer: TapGestureRecognizer()
..onTap = () {
showDialog<dynamic>(
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,
),
],
),
),
),
],
);
}
}

View file

@ -0,0 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/networking/http.dart';
final pHTTP = Provider((ref) => HTTP());

View file

@ -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/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/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/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_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/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/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_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/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/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_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_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_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/nodes_settings.dart';
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_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/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/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/node_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/events/global/wallet_sync_status_changed_event.dart';
@ -683,6 +686,18 @@ class RouteGenerator {
builder: (_) => const LanguageSettingsView(), builder: (_) => const LanguageSettingsView(),
settings: RouteSettings(name: settings.name)); 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: case AboutView.routeName:
return getRoute( return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute, shouldUseMaterialRoute: useMaterialPageRoute,
@ -1131,6 +1146,21 @@ class RouteGenerator {
} }
return _routeError("${settings.name} invalid args: ${args.toString()}"); return _routeError("${settings.name} invalid args: ${args.toString()}");
case NewWalletOptionsView.routeName:
if (args is Tuple2<String, Coin>) {
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: case RestoreWalletView.routeName:
if (args is Tuple5<String, Coin, int, DateTime, String>) { if (args is Tuple5<String, Coin, int, DateTime, String>) {
return getRoute( return getRoute(

View file

@ -12,12 +12,13 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:decimal/decimal.dart'; 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/crypto.dart';
import 'package:stackwallet/models/buy/response_objects/fiat.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/order.dart';
import 'package:stackwallet/models/buy/response_objects/quote.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/buy/buy_response.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fiat_enum.dart'; import 'package:stackwallet/utilities/enums/fiat_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
@ -35,8 +36,7 @@ class SimplexAPI {
static final SimplexAPI _instance = SimplexAPI._(); static final SimplexAPI _instance = SimplexAPI._();
static SimplexAPI get instance => _instance; static SimplexAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing HTTP client = HTTP();
http.Client? client;
Uri _buildUri(String path, Map<String, String>? params) { Uri _buildUri(String path, Map<String, String>? params) {
if (scheme == "http") { if (scheme == "http") {
@ -55,10 +55,15 @@ class SimplexAPI {
}; };
Uri url = _buildUri('api.php', data); Uri url = _buildUri('api.php', data);
var res = await http.post(url, headers: headers); var res = await client.post(
if (res.statusCode != 200) { url: url,
headers: headers,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (res.code != 200) {
throw Exception( throw Exception(
'getAvailableCurrencies exception: statusCode= ${res.statusCode}'); 'getAvailableCurrencies exception: statusCode= ${res.code}');
} }
final jsonArray = jsonDecode(res.body); // TODO handle if invalid json final jsonArray = jsonDecode(res.body); // TODO handle if invalid json
@ -116,10 +121,15 @@ class SimplexAPI {
}; };
Uri url = _buildUri('api.php', data); Uri url = _buildUri('api.php', data);
var res = await http.post(url, headers: headers); var res = await client.post(
if (res.statusCode != 200) { url: url,
headers: headers,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (res.code != 200) {
throw Exception( throw Exception(
'getAvailableCurrencies exception: statusCode= ${res.statusCode}'); 'getAvailableCurrencies exception: statusCode= ${res.code}');
} }
final jsonArray = jsonDecode(res.body); // TODO validate json final jsonArray = jsonDecode(res.body); // TODO validate json
@ -192,9 +202,14 @@ class SimplexAPI {
} }
Uri url = _buildUri('api.php', data); Uri url = _buildUri('api.php', data);
var res = await http.get(url, headers: headers); var res = await client.get(
if (res.statusCode != 200) { url: url,
throw Exception('getQuote exception: statusCode= ${res.statusCode}'); 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); final jsonArray = jsonDecode(res.body);
if (jsonArray.containsKey('error') as bool) { if (jsonArray.containsKey('error') as bool) {
@ -294,9 +309,14 @@ class SimplexAPI {
} }
Uri url = _buildUri('api.php', data); Uri url = _buildUri('api.php', data);
var res = await http.get(url, headers: headers); var res = await client.get(
if (res.statusCode != 200) { url: url,
throw Exception('newOrder exception: statusCode= ${res.statusCode}'); 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 final jsonArray = jsonDecode(res.body); // TODO check if valid json
if (jsonArray.containsKey('error') as bool) { if (jsonArray.containsKey('error') as bool) {

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart'; import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/db/hive/db.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/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_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/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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_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/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.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/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
@ -145,10 +146,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
Balance get balance => _balance ??= getCachedBalance(); Balance get balance => _balance ??= getCachedBalance();
Balance? _balance; Balance? _balance;
HTTP client = HTTP();
Future<String?> requestWork(String hash) async { Future<String?> requestWork(String hash) async {
return http return client
.post( .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'}, headers: {'Content-type': 'application/json'},
body: json.encode( body: json.encode(
{ {
@ -156,17 +159,19 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"hash": hash, "hash": hash,
}, },
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
) )
.then((http.Response response) { .then((client) {
if (response.statusCode == 200) { if (client.code == 200) {
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
json.decode(response.body) as Map<String, dynamic>; json.decode(client.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) { if (decoded.containsKey("error")) {
throw Exception("Received error ${decoded["error"]}"); throw Exception("Received error ${decoded["error"]}");
} }
return decoded["work"] as String?; return decoded["work"] as String?;
} else { } 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 = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final balanceResponse = await http.post( final balanceResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: balanceBody, body: balanceBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final balanceData = jsonDecode(balanceResponse.body); final balanceData = jsonDecode(balanceResponse.body);
@ -203,10 +210,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true", "representative": "true",
"account": publicAddress, "account": publicAddress,
}); });
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final String frontier = final String frontier =
@ -256,10 +265,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "send", "subtype": "send",
"block": sendBlock, "block": sendBlock,
}); });
final processResponse = await http.post( final processResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: processBody, body: processBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
@ -328,8 +339,13 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final response = await http.post(Uri.parse(getCurrentNode().host), final response = await client.post(
headers: headers, body: body); url: Uri.parse(getCurrentNode().host),
headers: headers,
body: body,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
_balance = Balance( _balance = Balance(
total: Amount( total: Amount(
@ -367,10 +383,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true", "representative": "true",
"account": publicAddress, "account": publicAddress,
}); });
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final infoData = jsonDecode(infoResponse.body); final infoData = jsonDecode(infoResponse.body);
@ -385,10 +403,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"account": publicAddress, "account": publicAddress,
}); });
final balanceResponse = await http.post( final balanceResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: balanceBody, body: balanceBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final balanceData = jsonDecode(balanceResponse.body); final balanceData = jsonDecode(balanceResponse.body);
@ -458,10 +478,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "receive", "subtype": "receive",
"block": receiveBlock, "block": receiveBlock,
}); });
final processResponse = await http.post( final processResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: processBody, body: processBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
@ -472,14 +494,18 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
Future<void> confirmAllReceivable() async { Future<void> confirmAllReceivable() async {
final receivableResponse = await http.post(Uri.parse(getCurrentNode().host), final receivableResponse = await client.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({ body: jsonEncode({
"action": "receivable", "action": "receivable",
"source": "true", "source": "true",
"account": await currentReceivingAddress, "account": await currentReceivingAddress,
"count": "-1", "count": "-1",
})); }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final receivableData = await jsonDecode(receivableResponse.body); final receivableData = await jsonDecode(receivableResponse.body);
if (receivableData["blocks"] == "") { if (receivableData["blocks"] == "") {
@ -501,13 +527,17 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
await confirmAllReceivable(); await confirmAllReceivable();
final receivingAddress = (await _currentReceivingAddress)!; final receivingAddress = (await _currentReceivingAddress)!;
final String publicAddress = receivingAddress.value; final String publicAddress = receivingAddress.value;
final response = await http.post(Uri.parse(getCurrentNode().host), final response = await client.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({ body: jsonEncode({
"action": "account_history", "action": "account_history",
"account": publicAddress, "account": publicAddress,
"count": "-1", "count": "-1",
})); }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final data = await jsonDecode(response.body); final data = await jsonDecode(response.body);
final transactions = final transactions =
data["history"] is List ? data["history"] as List<dynamic> : []; data["history"] is List ? data["history"] as List<dynamic> : [];
@ -600,7 +630,9 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
@ -817,17 +849,19 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<bool> testNetworkConnection() async { Future<bool> testNetworkConnection() async {
final uri = Uri.parse(getCurrentNode().host); final uri = Uri.parse(getCurrentNode().host);
final response = await http.post( final response = await client.post(
uri, url: uri,
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode( body: jsonEncode(
{ {
"action": "version", "action": "version",
}, },
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
return response.statusCode == 200; return response.code == 200;
} }
Timer? _networkAliveTimer; Timer? _networkAliveTimer;
@ -913,10 +947,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final infoData = jsonDecode(infoResponse.body); final infoData = jsonDecode(infoResponse.body);

View file

@ -1290,7 +1290,9 @@ class BitcoinWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1301,7 +1303,7 @@ class BitcoinWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1499,7 +1501,9 @@ class BitcoinWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1533,10 +1537,21 @@ class BitcoinWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: ""); await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: data?.mnemonicPassphrase ?? "",
);
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays
final initialAddresses = await Future.wait([ final initialAddresses = await Future.wait([

View file

@ -1186,7 +1186,9 @@ class BitcoinCashWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1196,7 +1198,7 @@ class BitcoinCashWallet extends CoinServiceAPI
} }
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1425,7 +1427,9 @@ class BitcoinCashWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1459,10 +1463,21 @@ class BitcoinCashWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: ""); await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: data?.mnemonicPassphrase ?? "",
);
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays
final initialAddresses = await Future.wait([ final initialAddresses = await Future.wait([

View file

@ -28,6 +28,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart';
import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart'; import 'package:stackwallet/services/coins/stellar/stellar_wallet.dart';
import 'package:stackwallet/services/coins/tezos/tezos_wallet.dart';
import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
@ -228,6 +229,24 @@ abstract class CoinServiceAPI {
tracker: tracker, tracker: tracker,
); );
case Coin.stellarTestnet:
return StellarWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
secureStore: secureStorageInterface,
tracker: tracker,
);
case Coin.tezos:
return TezosWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
secureStore: secureStorageInterface,
tracker: tracker,
);
case Coin.wownero: case Coin.wownero:
return WowneroWallet( return WowneroWallet(
walletId: walletId, walletId: walletId,
@ -285,15 +304,6 @@ abstract class CoinServiceAPI {
cachedClient: cachedClient, cachedClient: cachedClient,
tracker: tracker, tracker: tracker,
); );
case Coin.stellarTestnet:
return StellarWallet(
walletId: walletId,
walletName: walletName,
coin: coin,
secureStore: secureStorageInterface,
tracker: tracker,
);
} }
} }
@ -348,7 +358,9 @@ abstract class CoinServiceAPI {
required int height, required int height,
}); });
Future<void> initializeNew(); Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
);
Future<void> initializeExisting(); Future<void> initializeExisting();
Future<void> exit(); Future<void> exit();

View file

@ -1147,7 +1147,9 @@ class DogecoinWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1157,7 +1159,7 @@ class DogecoinWallet extends CoinServiceAPI
} }
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1349,7 +1351,9 @@ class DogecoinWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1383,12 +1387,20 @@ class DogecoinWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: data?.mnemonicPassphrase ?? "",
); );
// Generate and add addresses // Generate and add addresses

View file

@ -506,7 +506,9 @@ class ECashWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -544,10 +546,21 @@ class ECashWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: ""); await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: data?.mnemonicPassphrase ?? "",
);
const int startingIndex = 0; const int startingIndex = 0;
const int receiveChain = 0; const int receiveChain = 0;
@ -2778,7 +2791,9 @@ class ECashWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -2789,7 +2804,7 @@ class ECashWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);

View file

@ -176,7 +176,7 @@ Future<void> executeNative(Map<String, dynamic> arguments) async {
final selectionStrategyIsAll = final selectionStrategyIsAll =
arguments['selectionStrategyIsAll'] as int?; arguments['selectionStrategyIsAll'] as int?;
final minimumConfirmations = arguments['minimumConfirmations'] as int?; final minimumConfirmations = arguments['minimumConfirmations'] as int?;
final message = arguments['onChainNote'] as String?; final message = arguments['message'] as String?;
final amount = arguments['amount'] as int?; final amount = arguments['amount'] as int?;
final address = arguments['address'] as String?; final address = arguments['address'] as String?;
@ -421,26 +421,15 @@ class EpicCashWallet extends CoinServiceAPI
late SecureStorageInterface _secureStore; late SecureStorageInterface _secureStore;
Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
String? result;
try {
result = await cancelPendingTransaction(txSlateId);
Logging.instance.log("result?: $result", level: LogLevel.Info);
} catch (e, s) {
Logging.instance.log("$e, $s", level: LogLevel.Error);
}
return result!;
}
//
/// returns an empty String on success, error message on failure /// returns an empty String on success, error message on failure
Future<String> cancelPendingTransaction(String txSlateId) async { Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
final String wallet = try {
(await _secureStore.read(key: '${_walletId}_wallet'))!; final String wallet = (await _secureStore.read(
key: '${_walletId}_wallet',
))!;
String? result; final result = await m.protect(() async {
await m.protect(() async { return await compute(
result = await compute(
_cancelTransactionWrapper, _cancelTransactionWrapper,
Tuple2( Tuple2(
wallet, wallet,
@ -448,7 +437,15 @@ class EpicCashWallet extends CoinServiceAPI
), ),
); );
}); });
return result!; Logging.instance.log(
"cancel $txSlateId result: $result",
level: LogLevel.Info,
);
return result;
} catch (e, s) {
Logging.instance.log("$e, $s", level: LogLevel.Error);
return e.toString();
}
} }
@override @override
@ -492,7 +489,7 @@ class EpicCashWallet extends CoinServiceAPI
Logging.instance Logging.instance
.log("this is a string $message", level: LogLevel.Error); .log("this is a string $message", level: LogLevel.Error);
stop(receivePort); stop(receivePort);
throw Exception("txHttpSend isolate failed"); throw Exception(message);
} }
stop(receivePort); stop(receivePort);
Logging.instance Logging.instance
@ -667,25 +664,8 @@ class EpicCashWallet extends CoinServiceAPI
await epicUpdateLastScannedBlock(await getRestoreHeight()); await epicUpdateLastScannedBlock(await getRestoreHeight());
if (!await startScans()) { await _startScans();
refreshMutex = false;
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
NodeConnectionStatus.disconnected,
walletId,
coin,
),
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
return;
}
await refresh();
GlobalEventBus.instance.fire( GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent( WalletSyncStatusChangedEvent(
WalletSyncStatus.synced, WalletSyncStatus.synced,
@ -694,12 +674,23 @@ class EpicCashWallet extends CoinServiceAPI
), ),
); );
} catch (e, s) { } catch (e, s) {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
Logging.instance.log(
"Exception rethrown from fullRescan(): $e\n$s",
level: LogLevel.Error,
printFullLength: true,
);
rethrow;
} finally {
refreshMutex = false; refreshMutex = false;
Logging.instance
.log("$e, $s", level: LogLevel.Error, printFullLength: true);
} }
refreshMutex = false;
return;
} }
@override @override
@ -766,7 +757,9 @@ class EpicCashWallet extends CoinServiceAPI
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
await _prefs.init(); await _prefs.init();
await updateNode(false); await updateNode(false);
final mnemonic = await _getMnemonicList(); final mnemonic = await _getMnemonicList();
@ -1163,58 +1156,97 @@ class EpicCashWallet extends CoinServiceAPI
// TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed // TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed
} }
Future<bool> startScans() async { Future<void> _startScans() async {
try { try {
//First stop the current listener
if (ListenerManager.pointer != null) { if (ListenerManager.pointer != null) {
Logging.instance
.log("LISTENER HANDLER IS NOT NULL ....", level: LogLevel.Info);
Logging.instance
.log("STOPPING ANY WALLET LISTENER ....", level: LogLevel.Info);
epicboxListenerStop(ListenerManager.pointer!); epicboxListenerStop(ListenerManager.pointer!);
} }
final wallet = await _secureStore.read(key: '${_walletId}_wallet'); final wallet = await _secureStore.read(key: '${_walletId}_wallet');
var restoreHeight = epicGetRestoreHeight(); // max number of blocks to scan per loop iteration
var chainHeight = await this.chainHeight; const scanChunkSize = 10000;
if (epicGetLastScannedBlock() == null) {
await epicUpdateLastScannedBlock(await getRestoreHeight()); // force firing of scan progress event
}
int lastScannedBlock = epicGetLastScannedBlock()!;
const MAX_PER_LOOP = 10000;
await getSyncPercent; await getSyncPercent;
for (; lastScannedBlock < chainHeight;) {
chainHeight = await this.chainHeight; // fetch current chain height and last scanned block (should be the
lastScannedBlock = epicGetLastScannedBlock()!; // restore height if full rescan or a wallet restore)
int chainHeight = await this.chainHeight;
int lastScannedBlock =
epicGetLastScannedBlock() ?? await getRestoreHeight();
// loop while scanning in chain in chunks (of blocks?)
while (lastScannedBlock < chainHeight) {
Logging.instance.log( Logging.instance.log(
"chainHeight: $chainHeight, restoreHeight: $restoreHeight, lastScannedBlock: $lastScannedBlock", "chainHeight: $chainHeight, lastScannedBlock: $lastScannedBlock",
level: LogLevel.Info); level: LogLevel.Info,
int? nextScannedBlock; );
await m.protect(() async {
ReceivePort receivePort = await getIsolate({ final int nextScannedBlock = await m.protect(() async {
ReceivePort? receivePort;
try {
receivePort = await getIsolate({
"function": "scanOutPuts", "function": "scanOutPuts",
"wallet": wallet!, "wallet": wallet!,
"startHeight": lastScannedBlock, "startHeight": lastScannedBlock,
"numberOfBlocks": MAX_PER_LOOP, "numberOfBlocks": scanChunkSize,
}, name: walletName); }, name: walletName);
var message = await receivePort.first; // get response
final message = await receivePort.first;
// check for error message
if (message is String) { if (message is String) {
Logging.instance throw Exception("scanOutPuts isolate failed: $message");
.log("this is a string $message", level: LogLevel.Error);
stop(receivePort);
throw Exception("scanOutPuts isolate failed");
} }
nextScannedBlock = int.parse(message['outputs'] as String);
// attempt to grab next scanned block number
final nextScanned = int.tryParse(message['outputs'] as String);
if (nextScanned == null) {
throw Exception(
"scanOutPuts failed to parse next scanned block number from: $message",
);
}
return nextScanned;
} catch (_) {
rethrow;
} finally {
if (receivePort != null) {
// kill isolate
stop(receivePort); stop(receivePort);
Logging.instance }
.log('Closing scanOutPuts!\n $message', level: LogLevel.Info); }
}); });
await epicUpdateLastScannedBlock(nextScannedBlock!);
// update local cache
await epicUpdateLastScannedBlock(nextScannedBlock);
// force firing of scan progress event
await getSyncPercent; await getSyncPercent;
// update while loop condition variables
chainHeight = await this.chainHeight;
lastScannedBlock = nextScannedBlock;
} }
Logging.instance.log("successfully at the tip", level: LogLevel.Info);
Logging.instance.log(
"_startScans successfully at the tip",
level: LogLevel.Info,
);
//Once scanner completes restart listener
await listenToEpicbox(); await listenToEpicbox();
return true;
} catch (e, s) { } catch (e, s) {
Logging.instance.log("$e, $s", level: LogLevel.Warning); Logging.instance.log(
return false; "_startScans failed: $e\n$s",
level: LogLevel.Error,
);
rethrow;
} }
} }
@ -1494,24 +1526,7 @@ class EpicCashWallet extends CoinServiceAPI
final int curAdd = await setCurrentIndex(); final int curAdd = await setCurrentIndex();
await _getReceivingAddressForIndex(curAdd); await _getReceivingAddressForIndex(curAdd);
if (!await startScans()) { await _startScans();
refreshMutex = false;
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
NodeConnectionStatus.disconnected,
walletId,
coin,
),
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
return;
}
unawaited(startSync()); unawaited(startSync());
@ -1685,26 +1700,13 @@ class EpicCashWallet extends CoinServiceAPI
final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData = final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
[]; [];
// int latestTxnBlockHeight =
// DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight")
// as int? ??
// 0;
final slatesToCommits = await getSlatesToCommits(); final slatesToCommits = await getSlatesToCommits();
for (var tx in jsonTransactions) { for (var tx in jsonTransactions) {
Logging.instance.log("tx: $tx", level: LogLevel.Info); Logging.instance.log("tx: $tx", level: LogLevel.Info);
// // TODO: does "confirmed" mean finalized? If so please remove this todo // // TODO: does "confirmed" mean finalized? If so please remove this todo
final isConfirmed = tx["confirmed"] as bool; final isConfirmed = tx["confirmed"] as bool;
// // TODO: since we are now caching tx history in hive are we losing anything by skipping here?
// // TODO: we can skip this filtering if it causes issues as the cache is later merged with updated data anyways
// // this would just make processing and updating cache more efficient
// if (txHeight > 0 &&
// txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS &&
// isConfirmed) {
// continue;
// }
// Logging.instance.log("Transactions listed below");
// Logging.instance.log(jsonTransactions);
int amt = 0; int amt = 0;
if (tx["tx_type"] == "TxReceived" || if (tx["tx_type"] == "TxReceived" ||
tx["tx_type"] == "TxReceivedCancelled") { tx["tx_type"] == "TxReceivedCancelled") {
@ -1725,7 +1727,6 @@ class EpicCashWallet extends CoinServiceAPI
String? commitId = slatesToCommits[slateId]?['commitId'] as String?; String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
tx['numberOfMessages'] = tx['messages']?['messages']?.length; tx['numberOfMessages'] = tx['messages']?['messages']?.length;
tx['onChainNote'] = tx['messages']?['messages']?[0]?['message']; tx['onChainNote'] = tx['messages']?['messages']?[0]?['message'];
print("ON CHAIN MESSAGE IS ${tx['onChainNote']}");
int? height; int? height;
@ -1738,7 +1739,6 @@ class EpicCashWallet extends CoinServiceAPI
final isIncoming = (tx["tx_type"] == "TxReceived" || final isIncoming = (tx["tx_type"] == "TxReceived" ||
tx["tx_type"] == "TxReceivedCancelled"); tx["tx_type"] == "TxReceivedCancelled");
final txn = isar_models.Transaction( final txn = isar_models.Transaction(
walletId: walletId, walletId: walletId,
txid: commitId ?? tx["id"].toString(), txid: commitId ?? tx["id"].toString(),
@ -1763,7 +1763,9 @@ class EpicCashWallet extends CoinServiceAPI
otherData: tx['onChainNote'].toString(), otherData: tx['onChainNote'].toString(),
inputs: [], inputs: [],
outputs: [], outputs: [],
numberOfMessages: ((tx["numberOfMessages"] == null) ? 0 : tx["numberOfMessages"]) as int, numberOfMessages: ((tx["numberOfMessages"] == null)
? 0
: tx["numberOfMessages"]) as int,
); );
// txn.address = // txn.address =
@ -1805,24 +1807,7 @@ class EpicCashWallet extends CoinServiceAPI
} }
} }
//
// midSortedTx["inputSize"] = tx["num_inputs"];
// midSortedTx["outputSize"] = tx["num_outputs"];
// midSortedTx["aliens"] = <dynamic>[];
// midSortedTx["inputs"] = <dynamic>[];
// midSortedTx["outputs"] = <dynamic>[];
// key id not used afaik?
// midSortedTx["key_id"] = tx["parent_key_id"];
// if (txHeight >= latestTxnBlockHeight) {
// latestTxnBlockHeight = txHeight;
// }
txnsData.add(Tuple2(txn, transactionAddress)); txnsData.add(Tuple2(txn, transactionAddress));
// cachedMap?.remove(tx["id"].toString());
// cachedMap?.remove(commitId);
// Logging.instance.log("cmap: $cachedMap", level: LogLevel.Info);
} }
await db.addNewTransactionData(txnsData, walletId); await db.addNewTransactionData(txnsData, walletId);
@ -1837,57 +1822,6 @@ class EpicCashWallet extends CoinServiceAPI
), ),
); );
} }
// midSortedArray
// .sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int));
//
// final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]};
// final dateArray = <dynamic>[];
//
// for (int i = 0; i < midSortedArray.length; i++) {
// final txObject = midSortedArray[i];
// final date = extractDateFromTimestamp(txObject["timestamp"] as int);
//
// final txTimeArray = [txObject["timestamp"], date];
//
// if (dateArray.contains(txTimeArray[1])) {
// result["dateTimeChunks"].forEach((dynamic chunk) {
// if (extractDateFromTimestamp(chunk["timestamp"] as int) ==
// txTimeArray[1]) {
// if (chunk["transactions"] == null) {
// chunk["transactions"] = <Map<String, dynamic>>[];
// }
// chunk["transactions"].add(txObject);
// }
// });
// } else {
// dateArray.add(txTimeArray[1]);
//
// final chunk = {
// "timestamp": txTimeArray[0],
// "transactions": [txObject],
// };sendAll
//
// // result["dateTimeChunks"].
// result["dateTimeChunks"].add(chunk);
// }
// }
// final transactionsMap =
// TransactionData.fromJson(result).getAllTransactions();
// if (cachedMap != null) {
// transactionsMap.addAll(cachedMap);
// }
//
// final txModel = TransactionData.fromMap(transactionsMap);
//
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'storedTxnDataHeight',
// value: latestTxnBlockHeight);
// await DB.instance.put<dynamic>(
// boxName: walletId, key: 'latest_tx_model', value: txModel);
//
// return txModel;
} }
@override @override

View file

@ -310,7 +310,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance.log( Logging.instance.log(
"Generating new ${coin.prettyName} wallet.", "Generating new ${coin.prettyName} wallet.",
level: LogLevel.Info, level: LogLevel.Info,
@ -324,7 +326,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log( Logging.instance.log(
"Exception rethrown from initializeNew(): $e\n$s", "Exception rethrown from initializeNew(): $e\n$s",
@ -338,7 +340,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
]); ]);
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
// Logging.instance // Logging.instance
// .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); // .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
// if (!integrationTestFlag) { // if (!integrationTestFlag) {
@ -366,14 +370,23 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final String mnemonic = bip39.generateMnemonic(strength: 128); final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
final String mnemonic = bip39.generateMnemonic(strength: strength);
final String passphrase = data?.mnemonicPassphrase ?? "";
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: passphrase,
); );
await _generateAndSaveAddress(mnemonic, ""); await _generateAndSaveAddress(mnemonic, passphrase);
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
} }

View file

@ -1875,7 +1875,9 @@ class FiroWallet extends CoinServiceAPI
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1886,7 +1888,7 @@ class FiroWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -2124,7 +2126,9 @@ class FiroWallet extends CoinServiceAPI
} }
/// Generates initial wallet values such as mnemonic, chain (receive/change) arrays and indexes. /// Generates initial wallet values such as mnemonic, chain (receive/change) arrays and indexes.
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -2158,12 +2162,20 @@ class FiroWallet extends CoinServiceAPI
longMutex = false; longMutex = false;
throw Exception("Attempted to overwrite mnemonic on initialize new!"); throw Exception("Attempted to overwrite mnemonic on initialize new!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: data?.mnemonicPassphrase ?? "",
); );
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays
@ -3340,6 +3352,30 @@ class FiroWallet extends CoinServiceAPI
List<Map<String, dynamic>> allTransactions = []; List<Map<String, dynamic>> allTransactions = [];
// some lelantus transactions aren't fetched via wallet addresses so they
// will never show as confirmed in the gui.
final unconfirmedTransactions =
await db.getTransactions(walletId).filter().heightIsNull().findAll();
for (final tx in unconfirmedTransactions) {
final txn = await cachedElectrumXClient.getTransaction(
txHash: tx.txid,
verbose: true,
coin: coin,
);
final height = txn["height"] as int?;
if (height != null) {
// tx was mined
// add to allTxHashes
final info = {
"tx_hash": tx.txid,
"height": height,
"address": tx.address.value?.value,
};
allTxHashes.add(info);
}
}
// final currentHeight = await chainHeight; // final currentHeight = await chainHeight;
for (final txHash in allTxHashes) { for (final txHash in allTxHashes) {

View file

@ -1285,7 +1285,9 @@ class LitecoinWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1296,7 +1298,7 @@ class LitecoinWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1535,7 +1537,9 @@ class LitecoinWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1570,12 +1574,20 @@ class LitecoinWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: data?.mnemonicPassphrase ?? "",
); );
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays

View file

@ -181,7 +181,9 @@ class Manager with ChangeNotifier {
Future<bool> testNetworkConnection() => Future<bool> testNetworkConnection() =>
_currentWallet.testNetworkConnection(); _currentWallet.testNetworkConnection();
Future<void> initializeNew() => _currentWallet.initializeNew(); Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data) =>
_currentWallet.initializeNew(data);
Future<void> initializeExisting() => _currentWallet.initializeExisting(); Future<void> initializeExisting() => _currentWallet.initializeExisting();
Future<void> recoverFromMnemonic({ Future<void> recoverFromMnemonic({
required String mnemonic, required String mnemonic,

View file

@ -307,7 +307,9 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
await _prefs.init(); await _prefs.init();
// this should never fail // this should never fail

View file

@ -1268,7 +1268,9 @@ class NamecoinWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1279,7 +1281,7 @@ class NamecoinWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1517,7 +1519,9 @@ class NamecoinWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1544,12 +1548,20 @@ class NamecoinWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: data?.mnemonicPassphrase ?? "",
); );
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays

View file

@ -11,7 +11,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:nanodart/nanodart.dart'; import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/db/isar/main_db.dart';
@ -19,6 +18,7 @@ import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_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/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/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
@ -28,6 +28,7 @@ import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.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/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/constants.dart';
@ -154,10 +155,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
Balance get balance => _balance ??= getCachedBalance(); Balance get balance => _balance ??= getCachedBalance();
Balance? _balance; Balance? _balance;
HTTP client = HTTP();
Future<String?> requestWork(String hash) async { Future<String?> requestWork(String hash) async {
return http return client
.post( .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'}, headers: {'Content-type': 'application/json'},
body: json.encode( body: json.encode(
{ {
@ -165,9 +168,11 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"hash": hash, "hash": hash,
}, },
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
) )
.then((http.Response response) { .then((Response response) {
if (response.statusCode == 200) { if (response.code == 200) {
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
json.decode(response.body) as Map<String, dynamic>; json.decode(response.body) as Map<String, dynamic>;
if (decoded.containsKey("error")) { if (decoded.containsKey("error")) {
@ -175,7 +180,7 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
return decoded["work"] as String?; return decoded["work"] as String?;
} else { } else {
throw Exception("Received error ${response.statusCode}"); throw Exception("Received error ${response.body}");
} }
}); });
} }
@ -194,10 +199,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final balanceResponse = await http.post( final balanceResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: balanceBody, body: balanceBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final balanceData = jsonDecode(balanceResponse.body); final balanceData = jsonDecode(balanceResponse.body);
@ -212,10 +219,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true", "representative": "true",
"account": publicAddress, "account": publicAddress,
}); });
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final String frontier = final String frontier =
@ -265,10 +274,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "send", "subtype": "send",
"block": sendBlock, "block": sendBlock,
}); });
final processResponse = await http.post( final processResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: processBody, body: processBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
@ -333,8 +344,13 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final response = await http.post(Uri.parse(getCurrentNode().host), final response = await client.post(
headers: headers, body: body); url: Uri.parse(getCurrentNode().host),
headers: headers,
body: body,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final data = jsonDecode(response.body); final data = jsonDecode(response.body);
_balance = Balance( _balance = Balance(
total: Amount( total: Amount(
@ -372,10 +388,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"representative": "true", "representative": "true",
"account": publicAddress, "account": publicAddress,
}); });
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final infoData = jsonDecode(infoResponse.body); final infoData = jsonDecode(infoResponse.body);
@ -390,10 +408,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"account": publicAddress, "account": publicAddress,
}); });
final balanceResponse = await http.post( final balanceResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: balanceBody, body: balanceBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final balanceData = jsonDecode(balanceResponse.body); final balanceData = jsonDecode(balanceResponse.body);
@ -463,10 +483,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
"subtype": "receive", "subtype": "receive",
"block": receiveBlock, "block": receiveBlock,
}); });
final processResponse = await http.post( final processResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: processBody, body: processBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final Map<String, dynamic> decoded = final Map<String, dynamic> decoded =
@ -477,14 +499,18 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
Future<void> confirmAllReceivable() async { Future<void> confirmAllReceivable() async {
final receivableResponse = await http.post(Uri.parse(getCurrentNode().host), final receivableResponse = await client.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({ body: jsonEncode({
"action": "receivable", "action": "receivable",
"source": "true", "source": "true",
"account": await currentReceivingAddress, "account": await currentReceivingAddress,
"count": "-1", "count": "-1",
})); }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final receivableData = await jsonDecode(receivableResponse.body); final receivableData = await jsonDecode(receivableResponse.body);
if (receivableData["blocks"] == "") { if (receivableData["blocks"] == "") {
@ -506,13 +532,17 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
await confirmAllReceivable(); await confirmAllReceivable();
final receivingAddress = (await _currentReceivingAddress)!; final receivingAddress = (await _currentReceivingAddress)!;
final String publicAddress = receivingAddress.value; final String publicAddress = receivingAddress.value;
final response = await http.post(Uri.parse(getCurrentNode().host), final response = await client.post(
url: Uri.parse(getCurrentNode().host),
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({ body: jsonEncode({
"action": "account_history", "action": "account_history",
"account": publicAddress, "account": publicAddress,
"count": "-1", "count": "-1",
})); }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
final data = await jsonDecode(response.body); final data = await jsonDecode(response.body);
final transactions = final transactions =
data["history"] is List ? data["history"] as List<dynamic> : []; data["history"] is List ? data["history"] as List<dynamic> : [];
@ -607,7 +637,9 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
@ -828,17 +860,19 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<bool> testNetworkConnection() async { Future<bool> testNetworkConnection() async {
final uri = Uri.parse(getCurrentNode().host); final uri = Uri.parse(getCurrentNode().host);
final response = await http.post( final response = await client.post(
uri, url: uri,
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode( body: jsonEncode(
{ {
"action": "version", "action": "version",
}, },
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
return response.statusCode == 200; return response.code == 200;
} }
Timer? _networkAliveTimer; Timer? _networkAliveTimer;
@ -924,10 +958,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
final headers = { final headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
final infoResponse = await http.post( final infoResponse = await client.post(
Uri.parse(getCurrentNode().host), url: Uri.parse(getCurrentNode().host),
headers: headers, headers: headers,
body: infoBody, body: infoBody,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final infoData = jsonDecode(infoResponse.body); final infoData = jsonDecode(infoResponse.body);

View file

@ -1195,7 +1195,9 @@ class ParticlWallet extends CoinServiceAPI
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info); .log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
@ -1206,7 +1208,7 @@ class ParticlWallet extends CoinServiceAPI
await _prefs.init(); await _prefs.init();
try { try {
await _generateNewWallet(); await _generateNewWallet(data);
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s", Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal); level: LogLevel.Fatal);
@ -1432,7 +1434,9 @@ class ParticlWallet extends CoinServiceAPI
} }
} }
Future<void> _generateNewWallet() async { Future<void> _generateNewWallet(
({String mnemonicPassphrase, int wordCount})? data,
) async {
Logging.instance Logging.instance
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
if (!integrationTestFlag) { if (!integrationTestFlag) {
@ -1459,12 +1463,20 @@ class ParticlWallet extends CoinServiceAPI
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
} }
final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonic', key: '${_walletId}_mnemonic',
value: bip39.generateMnemonic(strength: 128)); value: bip39.generateMnemonic(strength: strength));
await _secureStore.write( await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase', key: '${_walletId}_mnemonicPassphrase',
value: "", value: data?.mnemonicPassphrase ?? "",
); );
// Generate and add addresses to relevant arrays // Generate and add addresses to relevant arrays

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:http/http.dart' as http; import 'package:bip39/bip39.dart' as bip39;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart' as SWBalance; import 'package:stackwallet/models/balance.dart' as SWBalance;
@ -28,6 +28,7 @@ import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart'; import 'package:stellar_flutter_sdk/stellar_flutter_sdk.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -53,20 +54,25 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
initCache(walletId, coin); initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride); initWalletDB(mockableOverride: mockableOverride);
if (coin.name == "stellarTestnet") { if (coin.isTestNet) {
stellarSdk = StellarSDK.TESTNET;
stellarNetwork = Network.TESTNET; stellarNetwork = Network.TESTNET;
} else { } else {
stellarSdk = StellarSDK.PUBLIC;
stellarNetwork = Network.PUBLIC; stellarNetwork = Network.PUBLIC;
} }
_updateNode();
}
void _updateNode() {
_xlmNode = NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
stellarSdk = StellarSDK("${_xlmNode!.host}:${_xlmNode!.port}");
} }
late final TransactionNotificationTracker txTracker; late final TransactionNotificationTracker txTracker;
late SecureStorageInterface _secureStore; late SecureStorageInterface _secureStore;
// final StellarSDK stellarSdk = StellarSDK.PUBLIC;
@override @override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite; bool? _isFavorite;
@ -175,6 +181,41 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
return exists; return exists;
} }
@override
Future<Map<String, dynamic>> prepareSend(
{required String address,
required Amount amount,
Map<String, dynamic>? args}) async {
try {
final feeRate = args?["feeRate"];
var fee = 1000;
if (feeRate is FeeRateType) {
final theFees = await fees;
switch (feeRate) {
case FeeRateType.fast:
fee = theFees.fast;
case FeeRateType.slow:
fee = theFees.slow;
case FeeRateType.average:
default:
fee = theFees.medium;
}
}
Map<String, dynamic> txData = {
"fee": fee,
"address": address,
"recipientAmt": amount,
"memo": args?["memo"] as String?,
};
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
return txData;
} catch (e, s) {
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
rethrow;
}
}
@override @override
Future<String> confirmSend({required Map<String, dynamic> txData}) async { Future<String> confirmSend({required Map<String, dynamic> txData}) async {
final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed'); final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed');
@ -182,34 +223,41 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
AccountResponse sender = AccountResponse sender =
await stellarSdk.accounts.account(senderKeyPair.accountId); await stellarSdk.accounts.account(senderKeyPair.accountId);
final amountToSend = txData['recipientAmt'] as Amount; final amountToSend = txData['recipientAmt'] as Amount;
final memo = txData["memo"] as String?;
//First check if account exists, can be skipped, but if the account does not exist, //First check if account exists, can be skipped, but if the account does not exist,
// the transaction fee will be charged when the transaction fails. // the transaction fee will be charged when the transaction fails.
bool validAccount = await _accountExists(txData['address'] as String); bool validAccount = await _accountExists(txData['address'] as String);
Transaction transaction; TransactionBuilder transactionBuilder;
if (!validAccount) { if (!validAccount) {
//Fund the account, user must ensure account is correct //Fund the account, user must ensure account is correct
CreateAccountOperationBuilder createAccBuilder = CreateAccountOperationBuilder createAccBuilder =
CreateAccountOperationBuilder( CreateAccountOperationBuilder(
txData['address'] as String, amountToSend.decimal.toString()); txData['address'] as String, amountToSend.decimal.toString());
transaction = TransactionBuilder(sender) transactionBuilder =
.addOperation(createAccBuilder.build()) TransactionBuilder(sender).addOperation(createAccBuilder.build());
.build();
} else { } else {
transaction = TransactionBuilder(sender) transactionBuilder = TransactionBuilder(sender).addOperation(
.addOperation(PaymentOperationBuilder(txData['address'] as String, PaymentOperationBuilder(txData['address'] as String, Asset.NATIVE,
Asset.NATIVE, amountToSend.decimal.toString()) amountToSend.decimal.toString())
.build()) .build());
.build();
} }
if (memo != null) {
transactionBuilder.addMemo(MemoText(memo));
}
final transaction = transactionBuilder.build();
transaction.sign(senderKeyPair, stellarNetwork); transaction.sign(senderKeyPair, stellarNetwork);
try { try {
SubmitTransactionResponse response = SubmitTransactionResponse response = await stellarSdk
await stellarSdk.submitTransaction(transaction); .submitTransaction(transaction)
.onError((error, stackTrace) => throw (error.toString()));
if (!response.success) { if (!response.success) {
throw ("Unable to send transaction"); throw ("${response.extras?.resultCodes?.transactionResultCode}"
" ::: ${response.extras?.resultCodes?.operationsResultCodes}");
} }
return response.hash!; return response.hash!;
} catch (e, s) { } catch (e, s) {
@ -232,32 +280,15 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
(await _currentReceivingAddress)?.value ?? await getAddressSW(); (await _currentReceivingAddress)?.value ?? await getAddressSW();
Future<int> getBaseFee() async { Future<int> getBaseFee() async {
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}"); var fees = await stellarSdk.feeStats.execute();
final nodeURI = Uri.parse(getCurrentNode().host); return int.parse(fees.lastLedgerBaseFee);
final httpClient = http.Client();
FeeStatsResponse fsp =
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
return int.parse(fsp.lastLedgerBaseFee);
} }
@override @override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async { Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
var baseFee = await getBaseFee(); var baseFee = await getBaseFee();
int fee = 100; return Amount(
switch (feeRate) { rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals);
case 0:
fee = baseFee * 10;
case 1:
case 2:
fee = baseFee * 50;
case 3:
fee = baseFee * 100;
case 4:
fee = baseFee * 200;
default:
fee = baseFee * 50;
}
return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals);
} }
@override @override
@ -285,34 +316,74 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
Future<FeeObject> get fees async { Future<FeeObject> get fees async {
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}"); int fee = await getBaseFee();
final nodeURI = Uri.parse(getCurrentNode().host);
final httpClient = http.Client();
FeeStatsResponse fsp =
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
return FeeObject( return FeeObject(
numberOfBlocksFast: 0, numberOfBlocksFast: 10,
numberOfBlocksAverage: 0, numberOfBlocksAverage: 10,
numberOfBlocksSlow: 0, numberOfBlocksSlow: 10,
fast: int.parse(fsp.lastLedgerBaseFee) * 100, fast: fee,
medium: int.parse(fsp.lastLedgerBaseFee) * 50, medium: fee,
slow: int.parse(fsp.lastLedgerBaseFee) * 10); slow: fee);
} }
@override @override
Future<void> fullRescan( Future<void> fullRescan(
int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async { int maxUnusedAddressGap,
await _prefs.init(); int maxNumberOfIndexesToCheck,
await updateTransactions(); ) async {
await updateChainHeight(); try {
await updateBalance(); Logging.instance.log("Starting full rescan!", level: LogLevel.Info);
longMutex = true;
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
final _mnemonic = await mnemonicString;
final _mnemonicPassphrase = await mnemonicPassphrase;
await db.deleteWalletBlockchainData(walletId);
await _recoverWalletFromBIP32SeedPhrase(
mnemonic: _mnemonic!,
mnemonicPassphrase: _mnemonicPassphrase!,
isRescan: true,
);
await refresh();
Logging.instance.log("Full rescan complete!", level: LogLevel.Info);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
} catch (e, s) {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
Logging.instance.log(
"Exception rethrown from fullRescan(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
} finally {
longMutex = false;
}
} }
@override @override
Future<bool> generateNewAddress() { Future<bool> generateNewAddress() {
// TODO: implement generateNewAddress // not used for stellar(?)
throw UnimplementedError(); throw UnimplementedError();
} }
@ -326,7 +397,9 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew() async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception( throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!"); "Attempted to overwrite mnemonic on generate new wallet!");
@ -334,11 +407,26 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
await _prefs.init(); await _prefs.init();
String mnemonic = await Wallet.generate24WordsMnemonic(); final int strength;
if (data == null || data.wordCount == 12) {
strength = 128;
} else if (data.wordCount == 24) {
strength = 256;
} else {
throw Exception("Invalid word count");
}
final String mnemonic = bip39.generateMnemonic(strength: strength);
final String passphrase = data?.mnemonicPassphrase ?? "";
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: ""); await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: passphrase,
);
Wallet wallet = await Wallet.from(mnemonic); Wallet wallet = await Wallet.from(
mnemonic,
passphrase: passphrase,
);
KeyPair keyPair = await wallet.getKeyPair(index: 0); KeyPair keyPair = await wallet.getKeyPair(index: 0);
String address = keyPair.accountId; String address = keyPair.accountId;
String secretSeed = String secretSeed =
@ -394,64 +482,24 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
Future<String?> get mnemonicString => Future<String?> get mnemonicString =>
_secureStore.read(key: '${_walletId}_mnemonic'); _secureStore.read(key: '${_walletId}_mnemonic');
@override Future<void> _recoverWalletFromBIP32SeedPhrase({
Future<Map<String, dynamic>> prepareSend( required String mnemonic,
{required String address, required String mnemonicPassphrase,
required Amount amount, bool isRescan = false,
Map<String, dynamic>? args}) async { }) async {
try { final Wallet wallet = await Wallet.from(
final feeRate = args?["feeRate"]; mnemonic,
var fee = 1000; passphrase: mnemonicPassphrase,
if (feeRate is FeeRateType) { );
final theFees = await fees; final KeyPair keyPair = await wallet.getKeyPair(index: 0);
switch (feeRate) { final String address = keyPair.accountId;
case FeeRateType.fast: String secretSeed =
fee = theFees.fast; keyPair.secretSeed; //This will be required for sending a tx
case FeeRateType.slow:
fee = theFees.slow; await _secureStore.write(
case FeeRateType.average: key: '${_walletId}_secretSeed',
default: value: secretSeed,
fee = theFees.medium;
}
}
Map<String, dynamic> txData = {
"fee": fee,
"address": address,
"recipientAmt": amount,
};
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
return txData;
} catch (e, s) {
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
rethrow;
}
}
@override
Future<void> recoverFromMnemonic(
{required String mnemonic,
String? mnemonicPassphrase,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
required int height}) async {
if ((await mnemonicString) != null ||
(await this.mnemonicPassphrase) != null) {
throw Exception("Attempted to overwrite mnemonic on restore!");
}
var wallet = await Wallet.from(mnemonic);
var keyPair = await wallet.getKeyPair(index: 0);
var address = keyPair.accountId;
var secretSeed = keyPair.secretSeed;
await _secureStore.write(
key: '${_walletId}_mnemonic', value: mnemonic.trim());
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: mnemonicPassphrase ?? "",
); );
await _secureStore.write(key: '${_walletId}_secretSeed', value: secretSeed);
final swAddress = SWAddress.Address( final swAddress = SWAddress.Address(
walletId: walletId, walletId: walletId,
@ -459,13 +507,62 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
publicKey: keyPair.publicKey, publicKey: keyPair.publicKey,
derivationIndex: 0, derivationIndex: 0,
derivationPath: null, derivationPath: null,
type: SWAddress.AddressType.unknown, // TODO: set type type: SWAddress.AddressType.unknown,
subType: SWAddress.AddressSubType.unknown); subType: SWAddress.AddressSubType.unknown,
);
if (isRescan) {
await db.updateOrPutAddresses([swAddress]);
} else {
await db.putAddress(swAddress); await db.putAddress(swAddress);
}
}
await Future.wait( bool longMutex = false;
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
@override
Future<void> recoverFromMnemonic({
required String mnemonic,
String? mnemonicPassphrase,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
required int height,
}) async {
longMutex = true;
try {
if ((await mnemonicString) != null ||
(await this.mnemonicPassphrase) != null) {
throw Exception("Attempted to overwrite mnemonic on restore!");
}
await _secureStore.write(
key: '${_walletId}_mnemonic',
value: mnemonic.trim(),
);
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: mnemonicPassphrase ?? "",
);
await _recoverWalletFromBIP32SeedPhrase(
mnemonic: mnemonic,
mnemonicPassphrase: mnemonicPassphrase ?? "",
isRescan: false,
);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverFromMnemonic(): $e\n$s",
level: LogLevel.Error);
rethrow;
} finally {
longMutex = false;
}
} }
Future<void> updateChainHeight() async { Future<void> updateChainHeight() async {
@ -482,26 +579,34 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
try { try {
List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>> List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>>
transactionList = []; transactionList = [];
Page<OperationResponse> payments;
Page<OperationResponse> payments = await stellarSdk.payments try {
payments = await stellarSdk.payments
.forAccount(await getAddressSW()) .forAccount(await getAddressSW())
.order(RequestBuilderOrder.DESC) .order(RequestBuilderOrder.DESC)
.execute() .execute()
.onError( .onError((error, stackTrace) => throw error!);
(error, stackTrace) => throw ("Could not fetch transactions")); } catch (e) {
if (e is ErrorResponse &&
e.body.contains("The resource at the url requested was not found. "
"This usually occurs for one of two reasons: "
"The url requested is not valid, or no data in our database "
"could be found with the parameters provided.")) {
// probably just doesn't have any history yet or whatever stellar needs
return;
} else {
Logging.instance.log(
"Stellar $walletName $walletId failed to fetch transactions",
level: LogLevel.Warning,
);
rethrow;
}
}
for (OperationResponse response in payments.records!) { for (OperationResponse response in payments.records!) {
// PaymentOperationResponse por; // PaymentOperationResponse por;
if (response is PaymentOperationResponse) { if (response is PaymentOperationResponse) {
PaymentOperationResponse por = response; PaymentOperationResponse por = response;
Logging.instance.log(
"ALL TRANSACTIONS IS ${por.transactionSuccessful}",
level: LogLevel.Info);
Logging.instance.log("THIS TX HASH IS ${por.transactionHash}",
level: LogLevel.Info);
SWTransaction.TransactionType type; SWTransaction.TransactionType type;
if (por.sourceAccount == await getAddressSW()) { if (por.sourceAccount == await getAddressSW()) {
type = SWTransaction.TransactionType.outgoing; type = SWTransaction.TransactionType.outgoing;
@ -628,13 +733,35 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
Logging.instance.log( Logging.instance.log(
"Exception rethrown from updateTransactions(): $e\n$s", "Exception rethrown from updateTransactions(): $e\n$s",
level: LogLevel.Error); level: LogLevel.Error);
rethrow;
} }
} }
Future<void> updateBalance() async { Future<void> updateBalance() async {
try { try {
AccountResponse accountResponse = AccountResponse accountResponse;
await stellarSdk.accounts.account(await getAddressSW());
try {
accountResponse = await stellarSdk.accounts
.account(await getAddressSW())
.onError((error, stackTrace) => throw error!);
} catch (e) {
if (e is ErrorResponse &&
e.body.contains("The resource at the url requested was not found. "
"This usually occurs for one of two reasons: "
"The url requested is not valid, or no data in our database "
"could be found with the parameters provided.")) {
// probably just doesn't have any history yet or whatever stellar needs
return;
} else {
Logging.instance.log(
"Stellar $walletName $walletId failed to fetch transactions",
level: LogLevel.Warning,
);
rethrow;
}
}
for (Balance balance in accountResponse.balances) { for (Balance balance in accountResponse.balances) {
switch (balance.assetType) { switch (balance.assetType) {
case Asset.TYPE_NATIVE: case Asset.TYPE_NATIVE:
@ -665,6 +792,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
"ERROR GETTING BALANCE $e\n$s", "ERROR GETTING BALANCE $e\n$s",
level: LogLevel.Info, level: LogLevel.Info,
); );
rethrow;
} }
} }
@ -739,9 +867,8 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
int get storedChainHeight => getCachedChainHeight(); int get storedChainHeight => getCachedChainHeight();
@override @override
Future<bool> testNetworkConnection() { Future<bool> testNetworkConnection() async {
// TODO: implement testNetworkConnection return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port);
throw UnimplementedError();
} }
@override @override
@ -750,10 +877,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
@override @override
Future<void> updateNode(bool shouldRefresh) async { Future<void> updateNode(bool shouldRefresh) async {
_xlmNode = NodeService(secureStorageInterface: _secureStore) _updateNode();
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
if (shouldRefresh) { if (shouldRefresh) {
unawaited(refresh()); unawaited(refresh());
} }
@ -795,7 +919,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
// TODO: implement utxos // not used
Future<List<UTXO>> get utxos => throw UnimplementedError(); Future<List<UTXO>> get utxos => throw UnimplementedError();
@override @override

View file

@ -0,0 +1,776 @@
import 'dart:async';
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.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';
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/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.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';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tezart/tezart.dart';
import 'package:tuple/tuple.dart';
const int MINIMUM_CONFIRMATIONS = 1;
const int _gasLimit = 10200;
class TezosWallet extends CoinServiceAPI with WalletCache, WalletDB {
TezosWallet({
required String walletId,
required String walletName,
required Coin coin,
required SecureStorageInterface secureStore,
required TransactionNotificationTracker tracker,
MainDB? mockableOverride,
}) {
txTracker = tracker;
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
}
NodeModel? _xtzNode;
NodeModel getCurrentNode() {
return _xtzNode ??
NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: Coin.tezos) ??
DefaultNodes.getNodeFor(Coin.tezos);
}
Future<Keystore> getKeystore() async {
return Keystore.fromMnemonic((await mnemonicString).toString());
}
@override
String get walletId => _walletId;
late String _walletId;
@override
String get walletName => _walletName;
late String _walletName;
@override
set walletName(String name) => _walletName = name;
@override
set isFavorite(bool markFavorite) {
_isFavorite = markFavorite;
updateCachedIsFavorite(markFavorite);
}
@override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite;
@override
Coin get coin => _coin;
late Coin _coin;
late SecureStorageInterface _secureStore;
late final TransactionNotificationTracker txTracker;
final _prefs = Prefs.instance;
Timer? timer;
bool _shouldAutoSync = false;
Timer? _networkAliveTimer;
@override
bool get shouldAutoSync => _shouldAutoSync;
HTTP client = HTTP();
@override
set shouldAutoSync(bool shouldAutoSync) {
if (_shouldAutoSync != shouldAutoSync) {
_shouldAutoSync = shouldAutoSync;
if (!shouldAutoSync) {
timer?.cancel();
timer = null;
stopNetworkAlivePinging();
} else {
startNetworkAlivePinging();
refresh();
}
}
}
void startNetworkAlivePinging() {
// call once on start right away
_periodicPingCheck();
// then periodically check
_networkAliveTimer = Timer.periodic(
Constants.networkAliveTimerDuration,
(_) async {
_periodicPingCheck();
},
);
}
void stopNetworkAlivePinging() {
_networkAliveTimer?.cancel();
_networkAliveTimer = null;
}
void _periodicPingCheck() async {
bool hasNetwork = await testNetworkConnection();
if (_isConnected != hasNetwork) {
NodeConnectionStatus status = hasNetwork
? NodeConnectionStatus.connected
: NodeConnectionStatus.disconnected;
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
status,
walletId,
coin,
),
);
_isConnected = hasNetwork;
if (hasNetwork) {
unawaited(refresh());
}
}
}
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
@override
Future<Map<String, dynamic>> prepareSend(
{required String address,
required Amount amount,
Map<String, dynamic>? args}) async {
try {
if (amount.decimals != coin.decimals) {
throw Exception("Amount decimals do not match coin decimals!");
}
var fee = int.parse((await estimateFeeFor(
amount, (args!["feeRate"] as FeeRateType).index))
.raw
.toString());
Map<String, dynamic> txData = {
"fee": fee,
"address": address,
"recipientAmt": amount,
};
return Future.value(txData);
} catch (e) {
return Future.error(e);
}
}
@override
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
try {
final amount = txData["recipientAmt"] as Amount;
final amountInMicroTez = amount.decimal * Decimal.fromInt(1000000);
final microtezToInt = int.parse(amountInMicroTez.toString());
final int feeInMicroTez = int.parse(txData["fee"].toString());
final String destinationAddress = txData["address"] as String;
final secretKey =
Keystore.fromMnemonic((await mnemonicString)!).secretKey;
Logging.instance.log(secretKey, level: LogLevel.Info);
final sourceKeyStore = Keystore.fromSecretKey(secretKey);
final client = TezartClient(getCurrentNode().host);
int? sendAmount = microtezToInt;
int gasLimit = _gasLimit;
int thisFee = feeInMicroTez;
if (balance.spendable == txData["recipientAmt"] as Amount) {
//Fee guides for emptying a tz account
// https://github.com/TezTech/eztz/blob/master/PROTO_004_FEES.md
thisFee = thisFee + 32;
sendAmount = microtezToInt - thisFee;
gasLimit = _gasLimit + 320;
}
final operation = await client.transferOperation(
source: sourceKeyStore,
destination: destinationAddress,
amount: sendAmount,
customFee: feeInMicroTez,
customGasLimit: gasLimit);
await operation.executeAndMonitor();
return operation.result.id as String;
} catch (e) {
Logging.instance.log(e.toString(), level: LogLevel.Error);
return Future.error(e);
}
}
@override
Future<String> get currentReceivingAddress async {
var mneString = await mnemonicString;
if (mneString == null) {
throw Exception("No mnemonic found!");
}
return Future.value((Keystore.fromMnemonic(mneString)).address);
}
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=1d";
var response = jsonDecode((await client.get(
url: Uri.parse(api),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
))
.body)[0];
double totalFees = response[4] as double;
int totalTxs = response[8] as int;
int feePerTx = (totalFees / totalTxs * 1000000).floor();
return Amount(
rawValue: BigInt.from(feePerTx),
fractionDigits: coin.decimals,
);
}
@override
Future<void> exit() {
_hasCalledExit = true;
return Future.value();
}
@override
Future<FeeObject> get fees async {
var api = "https://api.tzstats.com/series/op?start_date=today&collapse=10d";
var response = jsonDecode((await client.get(
url: Uri.parse(api),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
))
.body);
double totalFees = response[0][4] as double;
int totalTxs = response[0][8] as int;
int feePerTx = (totalFees / totalTxs * 1000000).floor();
Logging.instance.log("feePerTx:$feePerTx", level: LogLevel.Info);
// TODO: fix numberOfBlocks - Since there is only one fee no need to set blocks
return FeeObject(
numberOfBlocksFast: 10,
numberOfBlocksAverage: 10,
numberOfBlocksSlow: 10,
fast: feePerTx,
medium: feePerTx,
slow: feePerTx,
);
}
@override
Future<bool> generateNewAddress() {
// TODO: implement generateNewAddress
throw UnimplementedError();
}
@override
bool get hasCalledExit => _hasCalledExit;
bool _hasCalledExit = false;
@override
Future<void> initializeExisting() async {
await _prefs.init();
}
@override
Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data,
) async {
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!");
}
await _prefs.init();
var newKeystore = Keystore.random();
await _secureStore.write(
key: '${_walletId}_mnemonic',
value: newKeystore.mnemonic,
);
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: "",
);
final address = Address(
walletId: walletId,
value: newKeystore.address,
publicKey: [],
derivationIndex: 0,
derivationPath: null,
type: AddressType.unknown,
subType: AddressSubType.receiving,
);
await db.putAddress(address);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
}
@override
bool get isConnected => _isConnected;
bool _isConnected = false;
@override
bool get isRefreshing => refreshMutex;
bool refreshMutex = false;
@override
// TODO: implement maxFee
Future<int> get maxFee => throw UnimplementedError();
@override
Future<List<String>> get mnemonic async {
final mnemonic = await mnemonicString;
final mnemonicPassphrase = await this.mnemonicPassphrase;
if (mnemonic == null) {
throw Exception("No mnemonic found!");
}
if (mnemonicPassphrase == null) {
throw Exception("No mnemonic passphrase found!");
}
return mnemonic.split(" ");
}
@override
Future<String?> get mnemonicPassphrase =>
_secureStore.read(key: '${_walletId}_mnemonicPassphrase');
@override
Future<String?> get mnemonicString =>
_secureStore.read(key: '${_walletId}_mnemonic');
Future<void> _recoverWalletFromSeedPhrase({
required String mnemonic,
required String mnemonicPassphrase,
bool isRescan = false,
}) async {
final keystore = Keystore.fromMnemonic(
mnemonic,
password: mnemonicPassphrase,
);
final address = Address(
walletId: walletId,
value: keystore.address,
publicKey: [],
derivationIndex: 0,
derivationPath: null,
type: AddressType.unknown,
subType: AddressSubType.receiving,
);
if (isRescan) {
await db.updateOrPutAddresses([address]);
} else {
await db.putAddress(address);
}
}
bool longMutex = false;
@override
Future<void> fullRescan(
int maxUnusedAddressGap,
int maxNumberOfIndexesToCheck,
) async {
try {
Logging.instance.log("Starting full rescan!", level: LogLevel.Info);
longMutex = true;
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
final _mnemonic = await mnemonicString;
final _mnemonicPassphrase = await mnemonicPassphrase;
await db.deleteWalletBlockchainData(walletId);
await _recoverWalletFromSeedPhrase(
mnemonic: _mnemonic!,
mnemonicPassphrase: _mnemonicPassphrase!,
isRescan: true,
);
await refresh();
Logging.instance.log("Full rescan complete!", level: LogLevel.Info);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
} catch (e, s) {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
Logging.instance.log(
"Exception rethrown from fullRescan(): $e\n$s",
level: LogLevel.Error,
);
rethrow;
} finally {
longMutex = false;
}
}
@override
Future<void> recoverFromMnemonic({
required String mnemonic,
String? mnemonicPassphrase,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
required int height,
}) async {
longMutex = true;
try {
if ((await mnemonicString) != null ||
(await this.mnemonicPassphrase) != null) {
throw Exception("Attempted to overwrite mnemonic on restore!");
}
await _secureStore.write(
key: '${_walletId}_mnemonic', value: mnemonic.trim());
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: mnemonicPassphrase ?? "",
);
await _recoverWalletFromSeedPhrase(
mnemonic: mnemonic,
mnemonicPassphrase: mnemonicPassphrase ?? "",
isRescan: false,
);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
await refresh();
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverFromMnemonic(): $e\n$s",
level: LogLevel.Error);
rethrow;
} finally {
longMutex = false;
}
}
Future<void> updateBalance() async {
try {
String balanceCall = "https://api.mainnet.tzkt.io/v1/accounts/"
"${await currentReceivingAddress}/balance";
var response = jsonDecode(await client
.get(
url: Uri.parse(balanceCall),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.proxyInfo
: null,
)
.then((value) => value.body));
Amount balanceInAmount = Amount(
rawValue: BigInt.parse(response.toString()),
fractionDigits: coin.decimals);
_balance = Balance(
total: balanceInAmount,
spendable: balanceInAmount,
blockedTotal:
Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals),
pendingSpendable:
Amount(rawValue: BigInt.parse("0"), fractionDigits: coin.decimals),
);
await updateCachedBalance(_balance!);
} catch (e, s) {
Logging.instance
.log("ERROR GETTING BALANCE ${e.toString()}", level: LogLevel.Error);
}
}
Future<void> updateTransactions() async {
String transactionsCall = "https://api.mainnet.tzkt.io/v1/accounts/"
"${await currentReceivingAddress}/operations";
var response = jsonDecode(await client
.get(
url: Uri.parse(transactionsCall),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.proxyInfo
: null,
)
.then((value) => value.body));
List<Tuple2<Transaction, Address>> txs = [];
for (var tx in response as List) {
if (tx["type"] == "transaction") {
TransactionType txType;
final String myAddress = await currentReceivingAddress;
final String senderAddress = tx["sender"]["address"] as String;
final String targetAddress = tx["target"]["address"] as String;
if (senderAddress == myAddress && targetAddress == myAddress) {
txType = TransactionType.sentToSelf;
} else if (senderAddress == myAddress) {
txType = TransactionType.outgoing;
} else if (targetAddress == myAddress) {
txType = TransactionType.incoming;
} else {
txType = TransactionType.unknown;
}
var theTx = Transaction(
walletId: walletId,
txid: tx["hash"].toString(),
timestamp: DateTime.parse(tx["timestamp"].toString())
.toUtc()
.millisecondsSinceEpoch ~/
1000,
type: txType,
subType: TransactionSubType.none,
amount: tx["amount"] as int,
amountString: Amount(
rawValue:
BigInt.parse((tx["amount"] as int).toInt().toString()),
fractionDigits: coin.decimals)
.toJsonString(),
fee: tx["bakerFee"] as int,
height: int.parse(tx["level"].toString()),
isCancelled: false,
isLelantus: false,
slateId: "",
otherData: "",
inputs: [],
outputs: [],
nonce: 0,
numberOfMessages: null,
);
final AddressSubType subType;
switch (txType) {
case TransactionType.incoming:
case TransactionType.sentToSelf:
subType = AddressSubType.receiving;
break;
case TransactionType.outgoing:
case TransactionType.unknown:
subType = AddressSubType.unknown;
break;
}
final theAddress = Address(
walletId: walletId,
value: targetAddress,
publicKey: [],
derivationIndex: 0,
derivationPath: null,
type: AddressType.unknown,
subType: subType,
);
txs.add(Tuple2(theTx, theAddress));
}
}
Logging.instance.log("Transactions: $txs", level: LogLevel.Info);
await db.addNewTransactionData(txs, walletId);
}
Future<void> updateChainHeight() async {
try {
var api = "${getCurrentNode().host}/chains/main/blocks/head/header/shell";
var jsonParsedResponse = jsonDecode(await client
.get(
url: Uri.parse(api),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.proxyInfo
: null,
)
.then((value) => value.body));
final int intHeight = int.parse(jsonParsedResponse["level"].toString());
Logging.instance.log("Chain height: $intHeight", level: LogLevel.Info);
await updateCachedChainHeight(intHeight);
} catch (e, s) {
Logging.instance
.log("GET CHAIN HEIGHT ERROR ${e.toString()}", level: LogLevel.Error);
}
}
@override
Future<void> refresh() async {
if (refreshMutex) {
Logging.instance.log(
"$walletId $walletName refreshMutex denied",
level: LogLevel.Info,
);
return;
} else {
refreshMutex = true;
}
try {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
await updateChainHeight();
await updateBalance();
await updateTransactions();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
Logging.instance.log(
"Periodic refresh check for $walletId $walletName in object instance: $hashCode",
level: LogLevel.Info);
await refresh();
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"New data found in $walletId $walletName in background!",
walletId,
),
);
});
}
} catch (e, s) {
Logging.instance.log(
"Failed to refresh stellar wallet $walletId: '$walletName': $e\n$s",
level: LogLevel.Warning,
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
}
refreshMutex = false;
}
@override
int get storedChainHeight => getCachedChainHeight();
@override
Future<bool> testNetworkConnection() async {
try {
await client.get(
url: Uri.parse(
"${getCurrentNode().host}:${getCurrentNode().port}/chains/main/blocks/head/header/shell"),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
return true;
} catch (e) {
return false;
}
}
@override
Future<List<Transaction>> get transactions =>
db.getTransactions(walletId).findAll();
@override
Future<void> updateNode(bool shouldRefresh) async {
_xtzNode = NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
if (shouldRefresh) {
await refresh();
}
}
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final transaction = Transaction(
walletId: walletId,
txid: txData["txid"] as String,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: TransactionType.outgoing,
subType: TransactionSubType.none,
// precision may be lost here hence the following amountString
amount: (txData["recipientAmt"] as Amount).raw.toInt(),
amountString: (txData["recipientAmt"] as Amount).toJsonString(),
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
nonce: null,
inputs: [],
outputs: [],
numberOfMessages: null,
);
final address = txData["address"] is String
? await db.getAddress(walletId, txData["address"] as String)
: null;
await db.addNewTransactionData(
[
Tuple2(transaction, address),
],
walletId,
);
}
@override
// TODO: implement utxos
Future<List<UTXO>> get utxos => throw UnimplementedError();
@override
bool validateAddress(String address) {
return RegExp(r"^tz[1-9A-HJ-NP-Za-km-z]{34}$").hasMatch(address);
}
}

View file

@ -333,7 +333,10 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
} }
@override @override
Future<void> initializeNew({int seedWordsLength = 14}) async { Future<void> initializeNew(
({String mnemonicPassphrase, int wordCount})? data, {
int seedWordsLength = 14,
}) async {
await _prefs.init(); await _prefs.init();
// this should never fail // this should never fail

View file

@ -17,11 +17,14 @@ import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/eth_commons.dart'; import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart'; import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class EthApiException implements Exception { class EthApiException implements Exception {
@ -46,19 +49,23 @@ class EthereumResponse<T> {
abstract class EthereumAPI { abstract class EthereumAPI {
static String get stackBaseServer => DefaultNodes.ethereum.host; static String get stackBaseServer => DefaultNodes.ethereum.host;
static HTTP client = HTTP();
static Future<EthereumResponse<List<EthTxDTO>>> getEthTransactions({ static Future<EthereumResponse<List<EthTxDTO>>> getEthTransactions({
required String address, required String address,
int firstBlock = 0, int firstBlock = 0,
bool includeTokens = false, bool includeTokens = false,
}) async { }) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock", "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
if (response.body.isNotEmpty) { if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
final list = json["data"] as List?; final list = json["data"] as List?;
@ -86,7 +93,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getEthTransactions($address) failed with status code: " "getEthTransactions($address) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -173,13 +180,15 @@ abstract class EthereumAPI {
List<EthTxDTO> txns, List<EthTxDTO> txns,
) async { ) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
if (response.body.isNotEmpty) { if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
final list = List<Map<String, dynamic>>.from(json["data"] as List); final list = List<Map<String, dynamic>>.from(json["data"] as List);
@ -208,7 +217,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getEthTransactionNonces($txns) failed with status code: " "getEthTransactionNonces($txns) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -231,13 +240,15 @@ abstract class EthereumAPI {
static Future<EthereumResponse<List<EthTokenTxExtraDTO>>> static Future<EthereumResponse<List<EthTokenTxExtraDTO>>>
getEthTokenTransactionsByTxids(List<String> txids) async { getEthTokenTransactionsByTxids(List<String> txids) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/transactions?transactions=${txids.join(" ")}", "$stackBaseServer/transactions?transactions=${txids.join(" ")}",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
if (response.body.isNotEmpty) { if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
final list = json["data"] as List?; final list = json["data"] as List?;
@ -257,13 +268,13 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getEthTokenTransactionsByTxids($txids) response is empty but status code is " "getEthTokenTransactionsByTxids($txids) response is empty but status code is "
"${response.statusCode}", "${response.code}",
); );
} }
} else { } else {
throw EthApiException( throw EthApiException(
"getEthTokenTransactionsByTxids($txids) failed with status code: " "getEthTokenTransactionsByTxids($txids) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -288,13 +299,15 @@ abstract class EthereumAPI {
required String tokenContractAddress, required String tokenContractAddress,
}) async { }) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
if (response.body.isNotEmpty) { if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
final list = json["data"] as List?; final list = json["data"] as List?;
@ -321,7 +334,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getTokenTransactions($address, $tokenContractAddress) failed with status code: " "getTokenTransactions($address, $tokenContractAddress) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -422,9 +435,13 @@ abstract class EthereumAPI {
final uri = Uri.parse( final uri = Uri.parse(
"$stackBaseServer/tokens?addrs=$contractAddress $address", "$stackBaseServer/tokens?addrs=$contractAddress $address",
); );
final response = await get(uri); final response = await client.get(
url: uri,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.statusCode == 200) { if (response.code == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (json["data"] is List) { if (json["data"] is List) {
final map = json["data"].first as Map; final map = json["data"].first as Map;
@ -442,7 +459,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getWalletTokenBalance($address) failed with status code: " "getWalletTokenBalance($address) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -469,9 +486,13 @@ abstract class EthereumAPI {
final uri = Uri.parse( final uri = Uri.parse(
"$stackBaseServer/state?addrs=$address&parts=all", "$stackBaseServer/state?addrs=$address&parts=all",
); );
final response = await get(uri); final response = await client.get(
url: uri,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.statusCode == 200) { if (response.code == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
if (json["data"] is List) { if (json["data"] is List) {
final map = json["data"].first as Map; final map = json["data"].first as Map;
@ -488,7 +509,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getAddressNonce($address) failed with status code: " "getAddressNonce($address) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -510,13 +531,15 @@ abstract class EthereumAPI {
static Future<EthereumResponse<GasTracker>> getGasOracle() async { static Future<EthereumResponse<GasTracker>> getGasOracle() async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/gas-prices", "$stackBaseServer/gas-prices",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
if (json["success"] == true) { if (json["success"] == true) {
try { try {
@ -541,7 +564,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getGasOracle() failed with status code: " "getGasOracle() failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -579,13 +602,15 @@ abstract class EthereumAPI {
static Future<EthereumResponse<EthContract>> getTokenContractInfoByAddress( static Future<EthereumResponse<EthContract>> getTokenContractInfoByAddress(
String contractAddress) async { String contractAddress) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/tokens?addrs=$contractAddress&parts=all", "$stackBaseServer/tokens?addrs=$contractAddress&parts=all",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
final json = jsonDecode(response.body) as Map; final json = jsonDecode(response.body) as Map;
if (json["data"] is List) { if (json["data"] is List) {
final map = Map<String, dynamic>.from(json["data"].first as Map); final map = Map<String, dynamic>.from(json["data"].first as Map);
@ -621,7 +646,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getTokenByContractAddress($contractAddress) failed with status code: " "getTokenByContractAddress($contractAddress) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -646,13 +671,15 @@ abstract class EthereumAPI {
required String contractAddress, required String contractAddress,
}) async { }) async {
try { try {
final response = await get( final response = await client.get(
Uri.parse( url: Uri.parse(
"$stackBaseServer/abis?addrs=$contractAddress&verbose=true", "$stackBaseServer/abis?addrs=$contractAddress&verbose=true",
), ),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
final json = jsonDecode(response.body)["data"] as List; final json = jsonDecode(response.body)["data"] as List;
return EthereumResponse( return EthereumResponse(
@ -662,7 +689,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getTokenAbi($name, $contractAddress) failed with status code: " "getTokenAbi($name, $contractAddress) failed with status code: "
"${response.statusCode}", "${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {
@ -687,9 +714,13 @@ abstract class EthereumAPI {
String contractAddress, String contractAddress,
) async { ) async {
try { try {
final response = await get(Uri.parse( final response = await client.get(
"$stackBaseServer/state?addrs=$contractAddress&parts=proxy")); url: Uri.parse(
if (response.statusCode == 200) { "$stackBaseServer/state?addrs=$contractAddress&parts=proxy"),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.code == 200) {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
final list = json["data"] as List; final list = json["data"] as List;
final map = Map<String, dynamic>.from(list.first as Map); final map = Map<String, dynamic>.from(list.first as Map);
@ -701,7 +732,7 @@ abstract class EthereumAPI {
} else { } else {
throw EthApiException( throw EthApiException(
"getProxyTokenImplementationAddress($contractAddress) failed with" "getProxyTokenImplementationAddress($contractAddress) failed with"
" status code: ${response.statusCode}", " status code: ${response.code}",
); );
} }
} on EthApiException catch (e) { } on EthApiException catch (e) {

View file

@ -0,0 +1,23 @@
/*
* 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/utilities/logger.dart';
enum TorConnectionStatus { disconnected, connecting, connected }
class TorConnectionStatusChangedEvent {
TorConnectionStatus newStatus;
String message = "";
TorConnectionStatusChangedEvent(this.newStatus, this.message) {
Logging.instance.log(
"TorSyncStatusChangedEvent fired with arg newStatus = $newStatus ($message)",
level: LogLevel.Info);
}
}

View file

@ -0,0 +1,28 @@
/*
* 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/utilities/logger.dart';
enum TorStatus { enabled, disabled }
class TorPreferenceChangedEvent {
String? message;
TorStatus status;
TorPreferenceChangedEvent({
required this.status,
this.message,
}) {
Logging.instance.log(
"TorStatusChangedEvent changed to \"$status\" with message: $message",
level: LogLevel.Warning,
);
}
}

View file

@ -12,7 +12,6 @@ import 'dart:convert';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart'; import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart';
import 'package:stackwallet/exceptions/exchange/unsupported_currency_exception.dart'; import 'package:stackwallet/exceptions/exchange/unsupported_currency_exception.dart';
@ -26,9 +25,12 @@ import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.d
import 'package:stackwallet/models/exchange/response_objects/range.dart'; import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class ChangeNowAPI { class ChangeNowAPI {
@ -37,12 +39,13 @@ class ChangeNowAPI {
static const String apiVersion = "/v1"; static const String apiVersion = "/v1";
static const String apiVersionV2 = "/v2"; static const String apiVersionV2 = "/v2";
ChangeNowAPI._(); final HTTP client;
static final ChangeNowAPI _instance = ChangeNowAPI._();
static ChangeNowAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing @visibleForTesting
http.Client? client; ChangeNowAPI({HTTP? http}) : client = http ?? HTTP();
static final ChangeNowAPI _instance = ChangeNowAPI();
static ChangeNowAPI get instance => _instance;
Uri _buildUri(String path, Map<String, dynamic>? params) { Uri _buildUri(String path, Map<String, dynamic>? params) {
return Uri.https(authority, apiVersion + path, params); return Uri.https(authority, apiVersion + path, params);
@ -53,21 +56,23 @@ class ChangeNowAPI {
} }
Future<dynamic> _makeGetRequest(Uri uri) async { Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client();
try { try {
final response = await client.get( final response = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
String? data;
try { try {
final parsed = jsonDecode(response.body); data = response.body;
final parsed = jsonDecode(data);
return parsed; return parsed;
} on FormatException catch (e) { } on FormatException catch (e) {
return { return {
"error": "Dart format exception", "error": "Dart format exception",
"message": response.body, "message": data,
}; };
} }
} catch (e, s) { } catch (e, s) {
@ -78,17 +83,19 @@ class ChangeNowAPI {
} }
Future<dynamic> _makeGetRequestV2(Uri uri, String apiKey) async { Future<dynamic> _makeGetRequestV2(Uri uri, String apiKey) async {
final client = this.client ?? http.Client();
try { try {
final response = await client.get( final response = await client.get(
uri, url: uri,
headers: { headers: {
// 'Content-Type': 'application/json', // 'Content-Type': 'application/json',
'x-changenow-api-key': apiKey, 'x-changenow-api-key': apiKey,
}, },
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final parsed = jsonDecode(response.body); final data = response.body;
final parsed = jsonDecode(data);
return parsed; return parsed;
} catch (e, s) { } catch (e, s) {
@ -102,21 +109,24 @@ class ChangeNowAPI {
Uri uri, Uri uri,
Map<String, String> body, Map<String, String> body,
) async { ) async {
final client = this.client ?? http.Client();
try { try {
final response = await client.post( final response = await client.post(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(body), body: jsonEncode(body),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
String? data;
try { try {
final parsed = jsonDecode(response.body); data = response.body;
final parsed = jsonDecode(data);
return parsed; return parsed;
} catch (_) { } catch (_) {
Logging.instance.log("ChangeNOW api failed to parse: ${response.body}", Logging.instance
level: LogLevel.Error); .log("ChangeNOW api failed to parse: $data", level: LogLevel.Error);
rethrow; rethrow;
} }
} catch (e, s) { } catch (e, s) {

View file

@ -274,4 +274,15 @@ class ChangeNowExchange extends Exchange {
// TODO: implement getTrades // TODO: implement getTrades
throw UnimplementedError(); throw UnimplementedError();
} }
// ChangeNow does not support Tor.
//
// This code isn't required because the Exchange abstract class has a
// default implementation that returns false. This serves as an example and
// reminder in case ChangeNow files are copied to create a new exchange (or
// if ChangeNow ever supports Tor).
/*
@override
bool get supportsTor => true;
*/
} }

View file

@ -90,4 +90,25 @@ abstract class Exchange {
Estimate? estimate, Estimate? estimate,
required bool reversed, required bool reversed,
}); });
/// List of exchanges which support Tor.
///
/// Add to this list when adding a new exchange which supports Tor.
static List<Exchange> get exchangesWithTorSupport => [
MajesticBankExchange.instance,
TrocadorExchange.instance,
];
/// List of exchange names which support Tor.
///
/// Convenience method for when you just want to check for a String
/// .exchangeName instead of Exchange instances. Shouldn't need to be updated
/// as long as the above List is updated.
static List<String> get exchangeNamesWithTorSupport =>
exchangesWithTorSupport.map((exchange) => exchange.name).toList();
// Instead of using this, you can do like:
// currencies.removeWhere((element) =>
// !Exchange.exchangesWithTorSupport.any((e) => e.name == element.exchangeName)
// );
// But this helper may be more readable.
} }

View file

@ -20,6 +20,7 @@ import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchan
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -144,6 +145,8 @@ class ExchangeDataLoadingService {
); );
final start = DateTime.now(); final start = DateTime.now();
try { try {
/*
// Old exchange data loading code.
await Future.wait([ await Future.wait([
_loadChangeNowCurrencies(), _loadChangeNowCurrencies(),
// _loadChangeNowFixedRatePairs(), // _loadChangeNowFixedRatePairs(),
@ -157,6 +160,20 @@ class ExchangeDataLoadingService {
// quicker to load available currencies on the fly for a specific base currency // quicker to load available currencies on the fly for a specific base currency
// await _loadChangeNowFixedRatePairs(); // await _loadChangeNowFixedRatePairs();
// await _loadChangeNowEstimatedRatePairs(); // await _loadChangeNowEstimatedRatePairs();
*/
// If using Tor, don't load data for exchanges which don't support Tor.
//
// Add to this list when adding an exchange which doesn't supports Tor.
if (!Prefs.instance.useTor) {
await _loadChangeNowCurrencies();
}
// Exchanges which support Tor just get treated normally.
await Future.wait([
loadMajesticBankCurrencies(),
loadTrocadorCurrencies(),
]);
Logging.instance.log( Logging.instance.log(
"ExchangeDataLoadingService.loadAll finished in ${DateTime.now().difference(start).inSeconds} seconds", "ExchangeDataLoadingService.loadAll finished in ${DateTime.now().difference(start).inSeconds} seconds",

View file

@ -11,7 +11,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/exceptions/exchange/majestic_bank/mb_exception.dart'; import 'package:stackwallet/exceptions/exchange/majestic_bank/mb_exception.dart';
import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart'; import 'package:stackwallet/exceptions/exchange/pair_unavailable_exception.dart';
@ -20,8 +19,11 @@ import 'package:stackwallet/models/exchange/majestic_bank/mb_order.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order_calculation.dart'; import 'package:stackwallet/models/exchange/majestic_bank/mb_order_calculation.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order_status.dart'; import 'package:stackwallet/models/exchange/majestic_bank/mb_order_status.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_rate.dart'; import 'package:stackwallet/models/exchange/majestic_bank/mb_rate.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
class MajesticBankAPI { class MajesticBankAPI {
static const String scheme = "https"; static const String scheme = "https";
@ -35,22 +37,23 @@ class MajesticBankAPI {
static MajesticBankAPI get instance => _instance; static MajesticBankAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing HTTP client = HTTP();
http.Client? client;
Uri _buildUri({required String endpoint, Map<String, String>? params}) { Uri _buildUri({required String endpoint, Map<String, String>? params}) {
return Uri.https(authority, "/api/$version/$endpoint", params); return Uri.https(authority, "/api/$version/$endpoint", params);
} }
Future<dynamic> _makeGetRequest(Uri uri) async { Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client(); // final client = this.client ?? http.Client();
int code = -1; int code = -1;
try { try {
final response = await client.get( final response = await client.get(
uri, url: uri,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
code = response.statusCode; code = response.code;
final parsed = jsonDecode(response.body); final parsed = jsonDecode(response.body);

View file

@ -324,4 +324,8 @@ class MajesticBankExchange extends Exchange {
return ExchangeResponse(exception: response.exception); return ExchangeResponse(exception: response.exception);
} }
} }
// Majestic Bank supports tor.
@override
bool get supportsTor => true;
} }

View file

@ -12,7 +12,6 @@ import 'dart:convert';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/external_api_keys.dart'; import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart'; import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
@ -20,9 +19,12 @@ import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/models/exchange/simpleswap/sp_currency.dart'; import 'package:stackwallet/models/exchange/simpleswap/sp_currency.dart';
import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -34,22 +36,22 @@ class SimpleSwapAPI {
static final SimpleSwapAPI _instance = SimpleSwapAPI._(); static final SimpleSwapAPI _instance = SimpleSwapAPI._();
static SimpleSwapAPI get instance => _instance; static SimpleSwapAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing HTTP client = HTTP();
http.Client? client;
Uri _buildUri(String path, Map<String, String>? params) { Uri _buildUri(String path, Map<String, String>? params) {
return Uri.https(authority, path, params); return Uri.https(authority, path, params);
} }
Future<dynamic> _makeGetRequest(Uri uri) async { Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client();
int code = -1; int code = -1;
try { try {
final response = await client.get( final response = await client.get(
uri, url: uri,
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
code = response.statusCode; code = response.code;
final parsed = jsonDecode(response.body); final parsed = jsonDecode(response.body);
@ -67,15 +69,16 @@ class SimpleSwapAPI {
Uri uri, Uri uri,
Map<String, dynamic> body, Map<String, dynamic> body,
) async { ) async {
final client = this.client ?? http.Client();
try { try {
final response = await client.post( final response = await client.post(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(body), body: jsonEncode(body),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
if (response.statusCode == 200) { if (response.code == 200) {
final parsed = jsonDecode(response.body); final parsed = jsonDecode(response.body);
return parsed; return parsed;
} }

View file

@ -12,14 +12,16 @@ import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_native_splash/cli_commands.dart'; import 'package:flutter_native_splash/cli_commands.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_coin.dart'; import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_coin.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_rate.dart'; import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_rate.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade.dart'; import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade.dart';
import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade_new.dart'; import 'package:stackwallet/services/exchange/trocador/response_objects/trocador_trade_new.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
const kTrocadorApiKey = "8rFqf7QLxX1mUBiNPEMaLUpV2biz6n"; const kTrocadorApiKey = "8rFqf7QLxX1mUBiNPEMaLUpV2biz6n";
const kTrocadorRefCode = "9eHm9BkQfS"; const kTrocadorRefCode = "9eHm9BkQfS";
@ -31,6 +33,7 @@ abstract class TrocadorAPI {
static const String markup = "1"; static const String markup = "1";
static const String minKYCRating = "C"; static const String minKYCRating = "C";
static HTTP client = HTTP();
static Uri _buildUri({ static Uri _buildUri({
required String method, required String method,
@ -46,12 +49,14 @@ abstract class TrocadorAPI {
int code = -1; int code = -1;
try { try {
debugPrint("URI: $uri"); debugPrint("URI: $uri");
final response = await http.get( final response = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
code = response.statusCode; code = response.code;
debugPrint("CODE: $code"); debugPrint("CODE: $code");
debugPrint("BODY: ${response.body}"); debugPrint("BODY: ${response.body}");

View file

@ -401,4 +401,8 @@ class TrocadorExchange extends Exchange {
return ExchangeResponse(exception: response.exception); return ExchangeResponse(exception: response.exception);
} }
} }
// Trocador supports Tor.
@override
bool get supportsTor => true;
} }

View file

@ -1,8 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:stackwallet/dto/ordinals/inscription_data.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart';
import 'package:stackwallet/dto/ordinals/litescribe_response.dart'; import 'package:stackwallet/dto/ordinals/litescribe_response.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
class LitescribeAPI { class LitescribeAPI {
static final LitescribeAPI _instance = LitescribeAPI._internal(); static final LitescribeAPI _instance = LitescribeAPI._internal();
@ -13,12 +15,17 @@ class LitescribeAPI {
} }
LitescribeAPI._internal(); LitescribeAPI._internal();
HTTP client = HTTP();
late String baseUrl; late String baseUrl;
Future<LitescribeResponse> _getResponse(String endpoint) async { Future<LitescribeResponse> _getResponse(String endpoint) async {
final response = await http.get(Uri.parse('$baseUrl$endpoint')); final response = await client.get(
if (response.statusCode == 200) { url: Uri.parse('$baseUrl$endpoint'),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.code == 200) {
return LitescribeResponse(data: _validateJson(response.body)); return LitescribeResponse(data: _validateJson(response.body));
} else { } else {
throw Exception( throw Exception(

View file

@ -1,13 +1,16 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
final pMonKeyService = Provider((ref) => MonKeyService()); final pMonKeyService = Provider((ref) => MonKeyService());
class MonKeyService { class MonKeyService {
static const baseURL = "https://monkey.banano.cc/api/v1/monkey/"; static const baseURL = "https://monkey.banano.cc/api/v1/monkey/";
HTTP client = HTTP();
Future<Uint8List> fetchMonKey({ Future<Uint8List> fetchMonKey({
required String address, required String address,
@ -20,13 +23,17 @@ class MonKeyService {
url += '?format=png&size=512&background=false'; url += '?format=png&size=512&background=false';
} }
final response = await http.get(Uri.parse(url)); final response = await client.get(
url: Uri.parse(url),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
);
if (response.statusCode == 200) { if (response.code == 200) {
return response.bodyBytes; return Uint8List.fromList(response.bodyBytes);
} else { } else {
throw Exception( throw Exception(
"statusCode=${response.statusCode} body=${response.body}", "statusCode=${response.code} body=${response.body}",
); );
} }
} catch (e, s) { } catch (e, s) {

View file

@ -1,7 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:nanodart/nanodart.dart'; import 'package:nanodart/nanodart.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/prefs.dart';
class NanoAPI { class NanoAPI {
static Future< static Future<
@ -16,9 +18,11 @@ class NanoAPI {
NAccountInfo? accountInfo; NAccountInfo? accountInfo;
Exception? exception; Exception? exception;
HTTP client = HTTP();
try { try {
final response = await http.post( final response = await client.post(
server, url: server,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@ -27,6 +31,8 @@ class NanoAPI {
"representative": "true", "representative": "true",
"account": account, "account": account,
}), }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final map = jsonDecode(response.body); final map = jsonDecode(response.body);
@ -105,8 +111,10 @@ class NanoAPI {
required Uri server, required Uri server,
required Map<String, dynamic> block, required Map<String, dynamic> block,
}) async { }) async {
final response = await http.post( HTTP client = HTTP();
server,
final response = await client.post(
url: server,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@ -116,6 +124,8 @@ class NanoAPI {
"subtype": "change", "subtype": "change",
"block": block, "block": block,
}), }),
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
return jsonDecode(response.body); return jsonDecode(response.body);

View file

@ -13,8 +13,9 @@ import 'dart:convert';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/tor_service.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
@ -32,7 +33,7 @@ class PriceAPI {
static const Duration refreshIntervalDuration = static const Duration refreshIntervalDuration =
Duration(seconds: refreshInterval); Duration(seconds: refreshInterval);
final Client client; final HTTP client;
PriceAPI(this.client); PriceAPI(this.client);
@ -96,16 +97,18 @@ class PriceAPI {
} }
Map<Coin, Tuple2<Decimal, double>> result = {}; Map<Coin, Tuple2<Decimal, double>> result = {};
try { try {
final uri = final uri = Uri.parse(
Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" "https://api.coingecko.com/api/v3/coins/markets?vs_currency"
"=${baseCurrency.toLowerCase()}" "=${baseCurrency.toLowerCase()}"
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin," "&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar" "bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos"
"&order=market_cap_desc&per_page=50&page=1&sparkline=false"); "&order=market_cap_desc&per_page=50&page=1&sparkline=false");
final coinGeckoResponse = await client.get( final coinGeckoResponse = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>; final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>;
@ -136,6 +139,8 @@ class PriceAPI {
static Future<List<String>?> availableBaseCurrencies() async { static Future<List<String>?> availableBaseCurrencies() async {
final externalCalls = Prefs.instance.externalCalls; final externalCalls = Prefs.instance.externalCalls;
HTTP client = HTTP();
if ((!Logger.isTestEnv && !externalCalls) || if ((!Logger.isTestEnv && !externalCalls) ||
!(await Prefs.instance.isExternalCallsSet())) { !(await Prefs.instance.isExternalCallsSet())) {
Logging.instance.log("User does not want to use external calls", Logging.instance.log("User does not want to use external calls",
@ -146,9 +151,11 @@ class PriceAPI {
"https://api.coingecko.com/api/v3/simple/supported_vs_currencies"; "https://api.coingecko.com/api/v3/simple/supported_vs_currencies";
try { try {
final uri = Uri.parse(uriString); final uri = Uri.parse(uriString);
final response = await Client().get( final response = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final json = jsonDecode(response.body) as List<dynamic>; final json = jsonDecode(response.body) as List<dynamic>;
@ -186,8 +193,10 @@ class PriceAPI {
"=$contractAddressesString&include_24hr_change=true"); "=$contractAddressesString&include_24hr_change=true");
final coinGeckoResponse = await client.get( final coinGeckoResponse = await client.get(
uri, url: uri,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
proxyInfo:
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
); );
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map;

View file

@ -13,6 +13,7 @@ import 'dart:async';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:stackwallet/networking/http.dart';
import 'package:stackwallet/services/price.dart'; import 'package:stackwallet/services/price.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -29,7 +30,7 @@ class PriceService extends ChangeNotifier {
final Map<String, Tuple2<Decimal, double>> _cachedTokenPrices = {}; final Map<String, Tuple2<Decimal, double>> _cachedTokenPrices = {};
final _priceAPI = PriceAPI(Client()); final _priceAPI = PriceAPI(HTTP());
Tuple2<Decimal, double> getPrice(Coin coin) => _cachedPrices[coin]!; Tuple2<Decimal, double> getPrice(Coin coin) => _cachedPrices[coin]!;

View file

@ -0,0 +1,142 @@
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.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/utilities/logger.dart';
import 'package:tor_ffi_plugin/tor_ffi_plugin.dart';
final pTorService = Provider((_) => TorService.sharedInstance);
class TorService {
Tor? _tor;
/// Flag to indicate that a Tor circuit is thought to have been established.
bool _enabled = false;
/// Getter for the enabled flag.
bool get enabled => _enabled;
TorService._();
/// Singleton instance of the TorService.
///
/// Use this to access the TorService and its properties.
static final sharedInstance = TorService._();
/// Getter for the proxyInfo.
({
InternetAddress host,
int port,
}) get proxyInfo => (
host: InternetAddress.loopbackIPv4,
port: _tor!.port,
);
/// Initialize the tor ffi lib instance if it hasn't already been set. Nothing
/// changes if _tor is already been set.
void init({Tor? mockableOverride}) {
_tor ??= mockableOverride ?? Tor.instance;
}
/// Start the Tor service.
///
/// This will start the Tor service and establish a Tor circuit.
///
/// Throws an exception if the Tor service fails to start.
///
/// Returns a Future that completes when the Tor service has started.
Future<void> start() async {
if (_tor == null) {
throw Exception("TorService.init has not been called!");
}
if (_enabled) {
// already started so just return
// could throw an exception here or something so the caller
// is explicitly made aware of this
// TODO restart tor after that's been added to the tor-ffi crate
// (probably better to have a restart function separately)
// Fire a TorConnectionStatusChangedEvent on the event bus.
GlobalEventBus.instance.fire(
TorConnectionStatusChangedEvent(
TorConnectionStatus.connected,
"Tor connection status changed: connect ($_enabled)",
),
);
return;
}
// Start the Tor service.
try {
GlobalEventBus.instance.fire(
TorConnectionStatusChangedEvent(
TorConnectionStatus.connecting,
"Tor connection status changed: connecting",
),
);
await _tor!.start();
// no exception or error so we can (probably?) assume tor
// has started successfully
_enabled = true;
// Fire a TorConnectionStatusChangedEvent on the event bus.
GlobalEventBus.instance.fire(
TorConnectionStatusChangedEvent(
TorConnectionStatus.connected,
"Tor connection status changed: connect ($_enabled)",
),
);
} catch (e, s) {
Logging.instance.log(
"TorService.start failed: $e\n$s",
level: LogLevel.Warning,
);
// _enabled should already be false
// Fire a TorConnectionStatusChangedEvent on the event bus.
GlobalEventBus.instance.fire(
TorConnectionStatusChangedEvent(
TorConnectionStatus.disconnected,
"Tor connection status changed: $_enabled (failed)",
),
);
rethrow;
}
}
Future<void> stop() async {
if (_tor == null) {
throw Exception("TorService.init has not been called!");
}
if (!_enabled) {
// already stopped so just return
// could throw an exception here or something so the caller
// is explicitly made aware of this
// TODO make sure to kill
return;
}
// Stop the Tor service.
try {
_tor!.disable();
// no exception or error so we can (probably?) assume tor
// has started successfully
_enabled = false;
GlobalEventBus.instance.fire(
TorConnectionStatusChangedEvent(
TorConnectionStatus.disconnected,
"Tor connection status changed: $_enabled (disabled)",
),
);
} catch (e, s) {
Logging.instance.log(
"TorService.stop failed: $e\n$s",
level: LogLevel.Warning,
);
rethrow;
}
}
}

View file

@ -31,6 +31,7 @@ class CoinThemeColorDefault {
Color get stellar => const Color(0xFF6600FF); Color get stellar => const Color(0xFF6600FF);
Color get nano => const Color(0xFF209CE9); Color get nano => const Color(0xFF209CE9);
Color get banano => const Color(0xFFFBDD11); Color get banano => const Color(0xFFFBDD11);
Color get tezos => const Color(0xFF0F61FF);
Color forCoin(Coin coin) { Color forCoin(Coin coin) {
switch (coin) { switch (coin) {
@ -70,6 +71,8 @@ class CoinThemeColorDefault {
return nano; return nano;
case Coin.banano: case Coin.banano:
return banano; return banano;
case Coin.tezos:
return tezos;
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show more