Merge arti into fusion
6
.gitignore
vendored
|
@ -55,4 +55,10 @@ libcw_monero.dll
|
|||
libcw_wownero.dll
|
||||
libepic_cash_wallet.dll
|
||||
libmobileliblelantus.dll
|
||||
libtor_ffi.dll
|
||||
/libisar.so
|
||||
libtor_ffi.so
|
||||
|
||||
tor_logs.txt
|
||||
|
||||
torrc
|
||||
|
|
5
.gitmodules
vendored
|
@ -7,6 +7,9 @@
|
|||
[submodule "crypto_plugins/flutter_liblelantus"]
|
||||
path = crypto_plugins/flutter_liblelantus
|
||||
url = https://github.com/cypherstack/flutter_liblelantus.git
|
||||
[submodule "crypto_plugins/tor"]
|
||||
path = crypto_plugins/tor
|
||||
url = https://github.com/cypherstack/tor.git
|
||||
[submodule "fusiondart"]
|
||||
path = fusiondart
|
||||
url = https://github.com/cypherstack/fusiondart
|
||||
url = https://github.com/cypherstack/fusiondart.git
|
||||
|
|
4
assets/svg/connected-button.svg
Normal file
After Width: | Height: | Size: 9.1 KiB |
4
assets/svg/connecting-button.svg
Normal file
After Width: | Height: | Size: 11 KiB |
4
assets/svg/disconnected-button.svg
Normal file
After Width: | Height: | Size: 11 KiB |
5
assets/svg/tor-circle.svg
Normal 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 |
4
assets/svg/tor-synced.svg
Normal 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 |
4
assets/svg/tor-syncing.svg
Normal 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
|
@ -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
|
|
@ -123,7 +123,7 @@ flutter run android
|
|||
Note on Emulators: Only x86_64 emulators are supported, x86 emulators will not work
|
||||
|
||||
#### Linux
|
||||
Plug in your android device or use the emulator available via Android Studio and then run the following commands:
|
||||
Run the following commands or launch via Android Studio:
|
||||
```
|
||||
flutter pub get
|
||||
flutter run linux
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
||||
|
@ -158,6 +157,7 @@ class CachedElectrumX {
|
|||
|
||||
Future<List<String>> getUsedCoinSerials({
|
||||
required Coin coin,
|
||||
int startNumber = 0,
|
||||
}) async {
|
||||
try {
|
||||
final box = await DB.instance.getUsedSerialsCacheBox(coin: coin);
|
||||
|
@ -168,7 +168,7 @@ class CachedElectrumX {
|
|||
_list == null ? {} : List<String>.from(_list).toSet();
|
||||
|
||||
final startNumber =
|
||||
max(0, cachedSerials.length - 100); // 100 being some arbitrary buffer
|
||||
cachedSerials.length - 10; // 10 being some arbitrary buffer
|
||||
|
||||
final serials = await electrumXClient.getUsedCoinSerials(
|
||||
startNumber: startNumber,
|
||||
|
|
|
@ -8,13 +8,20 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/rpc.dart';
|
||||
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/tor_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
@ -65,12 +72,27 @@ class ElectrumX {
|
|||
JsonRPC? _rpcClient;
|
||||
|
||||
late Prefs _prefs;
|
||||
late TorService _torService;
|
||||
|
||||
List<ElectrumXNode>? failovers;
|
||||
int currentFailoverIndex = -1;
|
||||
|
||||
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
|
||||
|
||||
// add finalizer to cancel stream subscription when all references to an
|
||||
// instance of ElectrumX becomes inaccessible
|
||||
static final Finalizer<ElectrumX> _finalizer = Finalizer(
|
||||
(p0) {
|
||||
p0._torPreferenceListener?.cancel();
|
||||
p0._torStatusListener?.cancel();
|
||||
},
|
||||
);
|
||||
StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
|
||||
StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||
|
||||
final Mutex _torConnectingLock = Mutex();
|
||||
bool _requireMutex = false;
|
||||
|
||||
ElectrumX({
|
||||
required String host,
|
||||
required int port,
|
||||
|
@ -80,26 +102,79 @@ class ElectrumX {
|
|||
JsonRPC? client,
|
||||
this.connectionTimeoutForSpecialCaseJsonRPCClients =
|
||||
const Duration(seconds: 60),
|
||||
TorService? torService,
|
||||
EventBus? globalEventBusForTesting,
|
||||
}) {
|
||||
_prefs = prefs;
|
||||
_torService = torService ?? TorService.sharedInstance;
|
||||
_host = host;
|
||||
_port = port;
|
||||
_useSSL = useSSL;
|
||||
_rpcClient = client;
|
||||
|
||||
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
||||
_torStatusListener = bus.on<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({
|
||||
required ElectrumXNode node,
|
||||
required Prefs prefs,
|
||||
required List<ElectrumXNode> failovers,
|
||||
}) =>
|
||||
ElectrumX(
|
||||
host: node.address,
|
||||
port: node.port,
|
||||
useSSL: node.useSSL,
|
||||
prefs: prefs,
|
||||
failovers: failovers,
|
||||
);
|
||||
TorService? torService,
|
||||
EventBus? globalEventBusForTesting,
|
||||
}) {
|
||||
return ElectrumX(
|
||||
host: node.address,
|
||||
port: node.port,
|
||||
useSSL: node.useSSL,
|
||||
prefs: prefs,
|
||||
torService: torService,
|
||||
failovers: failovers,
|
||||
globalEventBusForTesting: globalEventBusForTesting,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _allow() async {
|
||||
if (_prefs.wifiOnly) {
|
||||
|
@ -109,6 +184,75 @@ class ElectrumX {
|
|||
return true;
|
||||
}
|
||||
|
||||
void _checkRpcClient() {
|
||||
// If we're supposed to use Tor...
|
||||
if (_prefs.useTor) {
|
||||
// But Tor isn't enabled...
|
||||
if (!_torService.enabled) {
|
||||
// And the killswitch isn't set...
|
||||
if (!_prefs.torKillSwitch) {
|
||||
// Then we'll just proceed and connect to ElectrumX through clearnet at the bottom of this function.
|
||||
Logging.instance.log(
|
||||
"Tor preference set but Tor is not enabled, killswitch not set, connecting to ElectrumX through clearnet",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
} else {
|
||||
// ... But if the killswitch is set, then we throw an exception.
|
||||
throw Exception(
|
||||
"Tor preference and killswitch set but Tor is not enabled, not connecting to ElectrumX");
|
||||
}
|
||||
} else {
|
||||
// Get the proxy info from the TorService.
|
||||
final proxyInfo = _torService.proxyInfo;
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
} else {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
}
|
||||
|
||||
if (_rpcClient!.proxyInfo != proxyInfo) {
|
||||
_rpcClient!.proxyInfo = proxyInfo;
|
||||
_rpcClient!.disconnect(
|
||||
reason: "Tor proxyInfo does not match current info",
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: null,
|
||||
);
|
||||
} else {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
proxyInfo: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send raw rpc command
|
||||
Future<dynamic> request({
|
||||
required String command,
|
||||
|
@ -121,20 +265,10 @@ class ElectrumX {
|
|||
throw WifiOnlyException();
|
||||
}
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
);
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
} else {
|
||||
_rpcClient = JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
);
|
||||
_checkRpcClient();
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -221,20 +355,10 @@ class ElectrumX {
|
|||
throw WifiOnlyException();
|
||||
}
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_rpcClient ??= JsonRPC(
|
||||
host: host,
|
||||
port: port,
|
||||
useSSL: useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
);
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
} else {
|
||||
_rpcClient = JsonRPC(
|
||||
host: failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
);
|
||||
_checkRpcClient();
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -14,7 +14,10 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.dart';
|
||||
import 'package:stackwallet/networking/socks_socket.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
||||
// Json RPC class to handle connecting to electrumx servers
|
||||
class JsonRPC {
|
||||
|
@ -23,16 +26,19 @@ class JsonRPC {
|
|||
required this.port,
|
||||
this.useSSL = false,
|
||||
this.connectionTimeout = const Duration(seconds: 60),
|
||||
required ({InternetAddress host, int port})? proxyInfo,
|
||||
});
|
||||
final bool useSSL;
|
||||
final String host;
|
||||
final int port;
|
||||
final Duration connectionTimeout;
|
||||
({InternetAddress host, int port})? proxyInfo;
|
||||
|
||||
final _requestMutex = Mutex();
|
||||
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
|
||||
Socket? _socket;
|
||||
StreamSubscription<Uint8List>? _subscription;
|
||||
SOCKSSocket? _socksSocket;
|
||||
StreamSubscription<List<int>>? _subscription;
|
||||
|
||||
void _dataHandler(List<int> data) {
|
||||
_requestQueue.nextIncompleteReq.then((req) {
|
||||
|
@ -75,7 +81,12 @@ class JsonRPC {
|
|||
_requestQueue.nextIncompleteReq.then((req) {
|
||||
if (req != null) {
|
||||
// \r\n required by electrumx server
|
||||
_socket!.write('${req.jsonRequest}\r\n');
|
||||
if (_socket != null) {
|
||||
_socket!.write('${req.jsonRequest}\r\n');
|
||||
}
|
||||
if (_socksSocket != null) {
|
||||
_socksSocket!.write('${req.jsonRequest}\r\n');
|
||||
}
|
||||
|
||||
// TODO different timeout length?
|
||||
req.initiateTimeout(
|
||||
|
@ -92,12 +103,22 @@ class JsonRPC {
|
|||
Duration requestTimeout,
|
||||
) async {
|
||||
await _requestMutex.protect(() async {
|
||||
if (_socket == null) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC request: opening socket $host:$port",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
await connect();
|
||||
if (!Prefs.instance.useTor) {
|
||||
if (_socket == null) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC request: opening socket $host:$port",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
await connect();
|
||||
}
|
||||
} else {
|
||||
if (_socksSocket == null) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC request: opening SOCKS socket to $host:$port",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
await connect();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -113,9 +134,9 @@ class JsonRPC {
|
|||
reason: "return req.completer.future.onError: $error\n$stackTrace",
|
||||
);
|
||||
return JsonRPCResponse(
|
||||
exception: error is Exception
|
||||
exception: error is JsonRpcException
|
||||
? error
|
||||
: Exception(
|
||||
: JsonRpcException(
|
||||
"req.completer.future.onError: $error\n$stackTrace",
|
||||
),
|
||||
);
|
||||
|
@ -137,6 +158,8 @@ class JsonRPC {
|
|||
_subscription = null;
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
await _socksSocket?.close();
|
||||
_socksSocket = null;
|
||||
|
||||
// clean up remaining queue
|
||||
await _requestQueue.completeRemainingWithError(
|
||||
|
@ -146,33 +169,84 @@ class JsonRPC {
|
|||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_socket != null) {
|
||||
throw Exception(
|
||||
"JsonRPC attempted to connect to an already existing socket!",
|
||||
);
|
||||
}
|
||||
if (!Prefs.instance.useTor) {
|
||||
if (useSSL) {
|
||||
_socket = await SecureSocket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
); // TODO do not automatically trust bad certificates
|
||||
} else {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: connectionTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
if (useSSL) {
|
||||
_socket = await SecureSocket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
); // TODO do not automatically trust bad certificates
|
||||
_subscription = _socket!.listen(
|
||||
_dataHandler,
|
||||
onError: _errorHandler,
|
||||
onDone: _doneHandler,
|
||||
cancelOnError: true,
|
||||
);
|
||||
} else {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: connectionTimeout,
|
||||
if (proxyInfo == null) {
|
||||
throw JsonRpcException(
|
||||
"JsonRPC.connect failed with useTor=${Prefs.instance.useTor} and proxyInfo is null");
|
||||
}
|
||||
|
||||
// instantiate a socks socket at localhost and on the port selected by the tor service
|
||||
_socksSocket = await SOCKSSocket.create(
|
||||
proxyHost: proxyInfo!.host.address,
|
||||
proxyPort: proxyInfo!.port,
|
||||
sslEnabled: useSSL,
|
||||
);
|
||||
|
||||
try {
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
|
||||
level: LogLevel.Info);
|
||||
|
||||
await _socksSocket?.connect();
|
||||
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...",
|
||||
level: LogLevel.Info);
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e",
|
||||
level: LogLevel.Error);
|
||||
throw JsonRpcException(
|
||||
"JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e");
|
||||
}
|
||||
|
||||
try {
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
|
||||
level: LogLevel.Info);
|
||||
|
||||
await _socksSocket?.connectTo(host, port);
|
||||
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
|
||||
level: LogLevel.Info);
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
|
||||
level: LogLevel.Error);
|
||||
throw JsonRpcException(
|
||||
"JsonRPC.connect(): failed to connect to tor proxy, $e");
|
||||
}
|
||||
|
||||
_subscription = _socksSocket!.listen(
|
||||
_dataHandler,
|
||||
onError: _errorHandler,
|
||||
onDone: _doneHandler,
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
|
||||
_subscription = _socket!.listen(
|
||||
_dataHandler,
|
||||
onError: _errorHandler,
|
||||
onDone: _doneHandler,
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,7 +351,7 @@ class _JsonRPCRequest {
|
|||
Future<void>.delayed(requestTimeout).then((_) {
|
||||
if (!isComplete) {
|
||||
try {
|
||||
throw Exception("_JsonRPCRequest timed out: $jsonRequest");
|
||||
throw JsonRpcException("_JsonRPCRequest timed out: $jsonRequest");
|
||||
} catch (e, s) {
|
||||
completer.completeError(e, s);
|
||||
onTimedOut?.call();
|
||||
|
@ -291,7 +365,18 @@ class _JsonRPCRequest {
|
|||
|
||||
class JsonRPCResponse {
|
||||
final dynamic data;
|
||||
final Exception? exception;
|
||||
final JsonRpcException? exception;
|
||||
|
||||
JsonRPCResponse({this.data, this.exception});
|
||||
}
|
||||
|
||||
bool isIpAddress(String host) {
|
||||
try {
|
||||
// if the string can be parsed into an InternetAddress, it's an IP.
|
||||
InternetAddress(host);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// if parsing fails, it's not an IP.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,324 +1,324 @@
|
|||
/*
|
||||
* This file is part of Stack Wallet.
|
||||
*
|
||||
* Copyright (c) 2023 Cypher Stack
|
||||
* All Rights Reserved.
|
||||
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
||||
* Generated by Cypher Stack on 2023-05-26
|
||||
*
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
|
||||
class ElectrumXSubscription with ChangeNotifier {
|
||||
dynamic _response;
|
||||
dynamic get response => _response;
|
||||
set response(dynamic newData) {
|
||||
_response = newData;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class SocketTask {
|
||||
SocketTask({this.completer, this.subscription});
|
||||
|
||||
final Completer<dynamic>? completer;
|
||||
final ElectrumXSubscription? subscription;
|
||||
|
||||
bool get isSubscription => subscription != null;
|
||||
}
|
||||
|
||||
class SubscribableElectrumXClient {
|
||||
int _currentRequestID = 0;
|
||||
bool _isConnected = false;
|
||||
List<int> _responseData = [];
|
||||
final Map<String, SocketTask> _tasks = {};
|
||||
Timer? _aliveTimer;
|
||||
Socket? _socket;
|
||||
late final bool _useSSL;
|
||||
late final Duration _connectionTimeout;
|
||||
late final Duration _keepAlive;
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
bool get useSSL => _useSSL;
|
||||
|
||||
void Function(bool)? onConnectionStatusChanged;
|
||||
|
||||
SubscribableElectrumXClient({
|
||||
bool useSSL = true,
|
||||
this.onConnectionStatusChanged,
|
||||
Duration connectionTimeout = const Duration(seconds: 5),
|
||||
Duration keepAlive = const Duration(seconds: 10),
|
||||
}) {
|
||||
_useSSL = useSSL;
|
||||
_connectionTimeout = connectionTimeout;
|
||||
_keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
Future<void> connect({required String host, required int port}) async {
|
||||
try {
|
||||
await _socket?.close();
|
||||
} catch (_) {}
|
||||
|
||||
if (_useSSL) {
|
||||
_socket = await SecureSocket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: _connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
);
|
||||
} else {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
port,
|
||||
timeout: _connectionTimeout,
|
||||
);
|
||||
}
|
||||
_updateConnectionStatus(true);
|
||||
|
||||
_socket!.listen(
|
||||
_dataHandler,
|
||||
onError: _errorHandler,
|
||||
onDone: _doneHandler,
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
_aliveTimer?.cancel();
|
||||
_aliveTimer = Timer.periodic(
|
||||
_keepAlive,
|
||||
(_) async => _updateConnectionStatus(await ping()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_aliveTimer?.cancel();
|
||||
await _socket?.close();
|
||||
onConnectionStatusChanged = null;
|
||||
}
|
||||
|
||||
String _buildJsonRequestString({
|
||||
required String method,
|
||||
required String id,
|
||||
required List<dynamic> params,
|
||||
}) {
|
||||
final paramString = jsonEncode(params);
|
||||
return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
|
||||
}
|
||||
|
||||
void _updateConnectionStatus(bool connectionStatus) {
|
||||
if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
|
||||
onConnectionStatusChanged!(connectionStatus);
|
||||
}
|
||||
_isConnected = connectionStatus;
|
||||
}
|
||||
|
||||
void _dataHandler(List<int> data) {
|
||||
_responseData.addAll(data);
|
||||
|
||||
// 0x0A is newline
|
||||
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
|
||||
if (data.last == 0x0A) {
|
||||
try {
|
||||
final response = jsonDecode(String.fromCharCodes(_responseData))
|
||||
as Map<String, dynamic>;
|
||||
_responseHandler(response);
|
||||
} catch (e, s) {
|
||||
Logging.instance
|
||||
.log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
|
||||
rethrow;
|
||||
} finally {
|
||||
_responseData = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _responseHandler(Map<String, dynamic> response) {
|
||||
// subscriptions will have a method in the response
|
||||
if (response['method'] is String) {
|
||||
_subscriptionHandler(response: response);
|
||||
return;
|
||||
}
|
||||
|
||||
final id = response['id'] as String;
|
||||
final result = response['result'];
|
||||
|
||||
_complete(id, result);
|
||||
}
|
||||
|
||||
void _subscriptionHandler({
|
||||
required Map<String, dynamic> response,
|
||||
}) {
|
||||
final method = response['method'];
|
||||
switch (method) {
|
||||
case "blockchain.scripthash.subscribe":
|
||||
final params = response["params"] as List<dynamic>;
|
||||
final scripthash = params.first as String;
|
||||
final taskId = "blockchain.scripthash.subscribe:$scripthash";
|
||||
|
||||
_tasks[taskId]?.subscription?.response = params.last;
|
||||
break;
|
||||
case "blockchain.headers.subscribe":
|
||||
final params = response["params"];
|
||||
const taskId = "blockchain.headers.subscribe";
|
||||
|
||||
_tasks[taskId]?.subscription?.response = params.first;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _errorHandler(Object error, StackTrace trace) {
|
||||
_updateConnectionStatus(false);
|
||||
Logging.instance.log(
|
||||
"SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
|
||||
level: LogLevel.Info);
|
||||
}
|
||||
|
||||
void _doneHandler() {
|
||||
_updateConnectionStatus(false);
|
||||
Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
|
||||
level: LogLevel.Info);
|
||||
}
|
||||
|
||||
void _complete(String id, dynamic data) {
|
||||
if (_tasks[id] == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
|
||||
_tasks[id]?.completer?.complete(data);
|
||||
}
|
||||
|
||||
if (!(_tasks[id]?.isSubscription ?? false)) {
|
||||
_tasks.remove(id);
|
||||
} else {
|
||||
_tasks[id]?.subscription?.response = data;
|
||||
}
|
||||
}
|
||||
|
||||
void _addTask({
|
||||
required String id,
|
||||
required Completer<dynamic> completer,
|
||||
}) {
|
||||
_tasks[id] = SocketTask(completer: completer, subscription: null);
|
||||
}
|
||||
|
||||
void _addSubscriptionTask({
|
||||
required String id,
|
||||
required ElectrumXSubscription subscription,
|
||||
}) {
|
||||
_tasks[id] = SocketTask(completer: null, subscription: subscription);
|
||||
}
|
||||
|
||||
Future<dynamic> _call({
|
||||
required String method,
|
||||
List<dynamic> params = const [],
|
||||
}) async {
|
||||
final completer = Completer<dynamic>();
|
||||
_currentRequestID++;
|
||||
final id = _currentRequestID.toString();
|
||||
_addTask(id: id, completer: completer);
|
||||
|
||||
_socket?.write(
|
||||
_buildJsonRequestString(
|
||||
method: method,
|
||||
id: id,
|
||||
params: params,
|
||||
),
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<dynamic> _callWithTimeout({
|
||||
required String method,
|
||||
List<dynamic> params = const [],
|
||||
Duration timeout = const Duration(seconds: 2),
|
||||
}) async {
|
||||
final completer = Completer<dynamic>();
|
||||
_currentRequestID++;
|
||||
final id = _currentRequestID.toString();
|
||||
_addTask(id: id, completer: completer);
|
||||
|
||||
_socket?.write(
|
||||
_buildJsonRequestString(
|
||||
method: method,
|
||||
id: id,
|
||||
params: params,
|
||||
),
|
||||
);
|
||||
|
||||
Timer(timeout, () {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(
|
||||
Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
ElectrumXSubscription _subscribe({
|
||||
required String taskId,
|
||||
required String method,
|
||||
List<dynamic> params = const [],
|
||||
}) {
|
||||
// try {
|
||||
final subscription = ElectrumXSubscription();
|
||||
_addSubscriptionTask(id: taskId, subscription: subscription);
|
||||
_currentRequestID++;
|
||||
_socket?.write(
|
||||
_buildJsonRequestString(
|
||||
method: method,
|
||||
id: taskId,
|
||||
params: params,
|
||||
),
|
||||
);
|
||||
|
||||
return subscription;
|
||||
// } catch (e, s) {
|
||||
// Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
|
||||
/// Ping the server to ensure it is responding
|
||||
///
|
||||
/// Returns true if ping succeeded
|
||||
Future<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: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
// /*
|
||||
// * This file is part of Stack Wallet.
|
||||
// *
|
||||
// * Copyright (c) 2023 Cypher Stack
|
||||
// * All Rights Reserved.
|
||||
// * The code is distributed under GPLv3 license, see LICENSE file for details.
|
||||
// * Generated by Cypher Stack on 2023-05-26
|
||||
// *
|
||||
// */
|
||||
//
|
||||
// import 'dart:async';
|
||||
// import 'dart:convert';
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:stackwallet/utilities/logger.dart';
|
||||
//
|
||||
// class ElectrumXSubscription with ChangeNotifier {
|
||||
// dynamic _response;
|
||||
// dynamic get response => _response;
|
||||
// set response(dynamic newData) {
|
||||
// _response = newData;
|
||||
// notifyListeners();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class SocketTask {
|
||||
// SocketTask({this.completer, this.subscription});
|
||||
//
|
||||
// final Completer<dynamic>? completer;
|
||||
// final ElectrumXSubscription? subscription;
|
||||
//
|
||||
// bool get isSubscription => subscription != null;
|
||||
// }
|
||||
//
|
||||
// class SubscribableElectrumXClient {
|
||||
// int _currentRequestID = 0;
|
||||
// bool _isConnected = false;
|
||||
// List<int> _responseData = [];
|
||||
// final Map<String, SocketTask> _tasks = {};
|
||||
// Timer? _aliveTimer;
|
||||
// Socket? _socket;
|
||||
// late final bool _useSSL;
|
||||
// late final Duration _connectionTimeout;
|
||||
// late final Duration _keepAlive;
|
||||
//
|
||||
// bool get isConnected => _isConnected;
|
||||
// bool get useSSL => _useSSL;
|
||||
//
|
||||
// void Function(bool)? onConnectionStatusChanged;
|
||||
//
|
||||
// SubscribableElectrumXClient({
|
||||
// bool useSSL = true,
|
||||
// this.onConnectionStatusChanged,
|
||||
// Duration connectionTimeout = const Duration(seconds: 5),
|
||||
// Duration keepAlive = const Duration(seconds: 10),
|
||||
// }) {
|
||||
// _useSSL = useSSL;
|
||||
// _connectionTimeout = connectionTimeout;
|
||||
// _keepAlive = keepAlive;
|
||||
// }
|
||||
//
|
||||
// Future<void> connect({required String host, required int port}) async {
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (_) {}
|
||||
//
|
||||
// if (_useSSL) {
|
||||
// _socket = await SecureSocket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// onBadCertificate: (_) => true,
|
||||
// );
|
||||
// } else {
|
||||
// _socket = await Socket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// );
|
||||
// }
|
||||
// _updateConnectionStatus(true);
|
||||
//
|
||||
// _socket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
//
|
||||
// _aliveTimer?.cancel();
|
||||
// _aliveTimer = Timer.periodic(
|
||||
// _keepAlive,
|
||||
// (_) async => _updateConnectionStatus(await ping()),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future<void> disconnect() async {
|
||||
// _aliveTimer?.cancel();
|
||||
// await _socket?.close();
|
||||
// onConnectionStatusChanged = null;
|
||||
// }
|
||||
//
|
||||
// String _buildJsonRequestString({
|
||||
// required String method,
|
||||
// required String id,
|
||||
// required List<dynamic> params,
|
||||
// }) {
|
||||
// final paramString = jsonEncode(params);
|
||||
// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
|
||||
// }
|
||||
//
|
||||
// void _updateConnectionStatus(bool connectionStatus) {
|
||||
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
|
||||
// onConnectionStatusChanged!(connectionStatus);
|
||||
// }
|
||||
// _isConnected = connectionStatus;
|
||||
// }
|
||||
//
|
||||
// void _dataHandler(List<int> data) {
|
||||
// _responseData.addAll(data);
|
||||
//
|
||||
// // 0x0A is newline
|
||||
// // https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html
|
||||
// if (data.last == 0x0A) {
|
||||
// try {
|
||||
// final response = jsonDecode(String.fromCharCodes(_responseData))
|
||||
// as Map<String, dynamic>;
|
||||
// _responseHandler(response);
|
||||
// } catch (e, s) {
|
||||
// Logging.instance
|
||||
// .log("JsonRPC jsonDecode: $e\n$s", level: LogLevel.Error);
|
||||
// rethrow;
|
||||
// } finally {
|
||||
// _responseData = [];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _responseHandler(Map<String, dynamic> response) {
|
||||
// // subscriptions will have a method in the response
|
||||
// if (response['method'] is String) {
|
||||
// _subscriptionHandler(response: response);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// final id = response['id'] as String;
|
||||
// final result = response['result'];
|
||||
//
|
||||
// _complete(id, result);
|
||||
// }
|
||||
//
|
||||
// void _subscriptionHandler({
|
||||
// required Map<String, dynamic> response,
|
||||
// }) {
|
||||
// final method = response['method'];
|
||||
// switch (method) {
|
||||
// case "blockchain.scripthash.subscribe":
|
||||
// final params = response["params"] as List<dynamic>;
|
||||
// final scripthash = params.first as String;
|
||||
// final taskId = "blockchain.scripthash.subscribe:$scripthash";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.last;
|
||||
// break;
|
||||
// case "blockchain.headers.subscribe":
|
||||
// final params = response["params"];
|
||||
// const taskId = "blockchain.headers.subscribe";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.first;
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _errorHandler(Object error, StackTrace trace) {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient called _errorHandler with: $error\n$trace",
|
||||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// void _doneHandler() {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
|
||||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// void _complete(String id, dynamic data) {
|
||||
// if (_tasks[id] == null) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
|
||||
// _tasks[id]?.completer?.complete(data);
|
||||
// }
|
||||
//
|
||||
// if (!(_tasks[id]?.isSubscription ?? false)) {
|
||||
// _tasks.remove(id);
|
||||
// } else {
|
||||
// _tasks[id]?.subscription?.response = data;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// void _addTask({
|
||||
// required String id,
|
||||
// required Completer<dynamic> completer,
|
||||
// }) {
|
||||
// _tasks[id] = SocketTask(completer: completer, subscription: null);
|
||||
// }
|
||||
//
|
||||
// void _addSubscriptionTask({
|
||||
// required String id,
|
||||
// required ElectrumXSubscription subscription,
|
||||
// }) {
|
||||
// _tasks[id] = SocketTask(completer: null, subscription: subscription);
|
||||
// }
|
||||
//
|
||||
// Future<dynamic> _call({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// }) async {
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// return completer.future;
|
||||
// }
|
||||
//
|
||||
// Future<dynamic> _callWithTimeout({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// Duration timeout = const Duration(seconds: 2),
|
||||
// }) async {
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// Timer(timeout, () {
|
||||
// if (!completer.isCompleted) {
|
||||
// completer.completeError(
|
||||
// Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return completer.future;
|
||||
// }
|
||||
//
|
||||
// ElectrumXSubscription _subscribe({
|
||||
// required String taskId,
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// }) {
|
||||
// // try {
|
||||
// final subscription = ElectrumXSubscription();
|
||||
// _addSubscriptionTask(id: taskId, subscription: subscription);
|
||||
// _currentRequestID++;
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: taskId,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// return subscription;
|
||||
// // } catch (e, s) {
|
||||
// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
|
||||
// // return null;
|
||||
// // }
|
||||
// }
|
||||
//
|
||||
// /// Ping the server to ensure it is responding
|
||||
// ///
|
||||
// /// Returns true if ping succeeded
|
||||
// Future<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: [],
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
21
lib/exceptions/json_rpc/json_rpc_exception.dart
Normal 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;
|
||||
}
|
|
@ -16,7 +16,6 @@ import 'package:cw_core/node.dart';
|
|||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_libmonero/monero/monero.dart';
|
||||
|
@ -59,6 +58,7 @@ import 'package:stackwallet/services/locale_service.dart';
|
|||
import 'package:stackwallet/services/node_service.dart';
|
||||
import 'package:stackwallet/services/notifications_api.dart';
|
||||
import 'package:stackwallet/services/notifications_service.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/services/trade_service.dart';
|
||||
import 'package:stackwallet/themes/theme_providers.dart';
|
||||
import 'package:stackwallet/themes/theme_service.dart';
|
||||
|
@ -97,7 +97,7 @@ void main() async {
|
|||
setWindowMaxSize(Size.infinite);
|
||||
|
||||
final screenHeight = screen?.frame.height;
|
||||
if (screenHeight != null && !kDebugMode) {
|
||||
if (screenHeight != null) {
|
||||
// starting to height be 3/4 screen height or 900, whichever is smaller
|
||||
final height = min<double>(screenHeight * 0.75, 900);
|
||||
setWindowFrame(
|
||||
|
@ -167,6 +167,16 @@ void main() async {
|
|||
await Hive.openBox<dynamic>(DB.boxNamePrefs);
|
||||
await Prefs.instance.init();
|
||||
|
||||
// TODO:
|
||||
// This should be moved to happen during the loading animation instead of
|
||||
// showing a blank screen for 4-10 seconds.
|
||||
// Some refactoring will need to be done here to make sure we don't make any
|
||||
// network calls before starting up tor
|
||||
if (Prefs.instance.useTor) {
|
||||
TorService.sharedInstance.init();
|
||||
await TorService.sharedInstance.start();
|
||||
}
|
||||
|
||||
await StackFileSystem.initThemesDir();
|
||||
|
||||
// Desktop migrate handled elsewhere (currently desktop_login_view.dart)
|
||||
|
|
124
lib/networking/http.dart
Normal 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;
|
||||
}
|
||||
}
|
343
lib/networking/socks_socket.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,10 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
|
||||
|
@ -16,6 +19,7 @@ import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
|
|||
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/themes/theme_providers.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
|
@ -30,16 +34,17 @@ class AddTokenListElementData {
|
|||
bool selected = false;
|
||||
}
|
||||
|
||||
class AddTokenListElement extends StatefulWidget {
|
||||
class AddTokenListElement extends ConsumerStatefulWidget {
|
||||
const AddTokenListElement({Key? key, required this.data}) : super(key: key);
|
||||
|
||||
final AddTokenListElementData data;
|
||||
|
||||
@override
|
||||
State<AddTokenListElement> createState() => _AddTokenListElementState();
|
||||
ConsumerState<AddTokenListElement> createState() =>
|
||||
_AddTokenListElementState();
|
||||
}
|
||||
|
||||
class _AddTokenListElementState extends State<AddTokenListElement> {
|
||||
class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
|
||||
@override
|
||||
|
@ -74,6 +79,17 @@ class _AddTokenListElementState extends State<AddTokenListElement> {
|
|||
currency.image,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
placeholderBuilder: (_) => SvgPicture.file(
|
||||
File(
|
||||
ref.watch(
|
||||
themeAssetsProvider.select(
|
||||
(value) => value.stackIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
),
|
||||
)
|
||||
: SvgPicture.asset(
|
||||
widget.data.token.symbol == "BNB"
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
|
@ -336,46 +337,60 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
|||
ref.read(walletsServiceChangeNotifierProvider);
|
||||
final name = textEditingController.text;
|
||||
|
||||
if (await walletsService.checkForDuplicate(name)) {
|
||||
unawaited(showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Wallet name already in use.",
|
||||
iconAsset: Assets.svg.circleAlert,
|
||||
context: context,
|
||||
));
|
||||
} else {
|
||||
// hide keyboard if has focus
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 50));
|
||||
}
|
||||
final hasDuplicateName =
|
||||
await walletsService.checkForDuplicate(name);
|
||||
|
||||
if (mounted) {
|
||||
switch (widget.addWalletType) {
|
||||
case AddWalletType.New:
|
||||
unawaited(Navigator.of(context).pushNamed(
|
||||
NewWalletRecoveryPhraseWarningView.routeName,
|
||||
arguments: Tuple2(
|
||||
name,
|
||||
coin,
|
||||
),
|
||||
));
|
||||
break;
|
||||
case AddWalletType.Restore:
|
||||
ref
|
||||
.read(mnemonicWordCountStateProvider.state)
|
||||
.state = Constants.possibleLengthsForCoin(
|
||||
coin)
|
||||
.first;
|
||||
unawaited(Navigator.of(context).pushNamed(
|
||||
RestoreOptionsView.routeName,
|
||||
arguments: Tuple2(
|
||||
name,
|
||||
coin,
|
||||
),
|
||||
));
|
||||
break;
|
||||
if (mounted) {
|
||||
if (hasDuplicateName) {
|
||||
unawaited(showFloatingFlushBar(
|
||||
type: FlushBarType.warning,
|
||||
message: "Wallet name already in use.",
|
||||
iconAsset: Assets.svg.circleAlert,
|
||||
context: context,
|
||||
));
|
||||
} else {
|
||||
// hide keyboard if has focus
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ref
|
||||
.read(mnemonicWordCountStateProvider.state)
|
||||
.state =
|
||||
Constants.possibleLengthsForCoin(coin).last;
|
||||
ref.read(pNewWalletOptions.notifier).state = null;
|
||||
|
||||
switch (widget.addWalletType) {
|
||||
case AddWalletType.New:
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
coin.hasMnemonicPassphraseSupport
|
||||
? NewWalletOptionsView.routeName
|
||||
: NewWalletRecoveryPhraseWarningView
|
||||
.routeName,
|
||||
arguments: Tuple2(
|
||||
name,
|
||||
coin,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case AddWalletType.Restore:
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
RestoreOptionsView.routeName,
|
||||
arguments: Tuple2(
|
||||
name,
|
||||
coin,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import 'dart:async';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/recovery_phrase_explanation_dialog.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
|
@ -38,7 +39,7 @@ import 'package:stackwallet/widgets/rounded_container.dart';
|
|||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
|
||||
class NewWalletRecoveryPhraseWarningView extends ConsumerStatefulWidget {
|
||||
const NewWalletRecoveryPhraseWarningView({
|
||||
Key? key,
|
||||
required this.coin,
|
||||
|
@ -51,12 +52,12 @@ class NewWalletRecoveryPhraseWarningView extends StatefulWidget {
|
|||
final String walletName;
|
||||
|
||||
@override
|
||||
State<NewWalletRecoveryPhraseWarningView> createState() =>
|
||||
ConsumerState<NewWalletRecoveryPhraseWarningView> createState() =>
|
||||
_NewWalletRecoveryPhraseWarningViewState();
|
||||
}
|
||||
|
||||
class _NewWalletRecoveryPhraseWarningViewState
|
||||
extends State<NewWalletRecoveryPhraseWarningView> {
|
||||
extends ConsumerState<NewWalletRecoveryPhraseWarningView> {
|
||||
late final Coin coin;
|
||||
late final String walletName;
|
||||
late final bool isDesktop;
|
||||
|
@ -72,6 +73,10 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
final options = ref.read(pNewWalletOptions.state).state;
|
||||
|
||||
final seedCount = options?.mnemonicWordsCount ??
|
||||
Constants.defaultSeedPhraseLengthFor(coin: coin);
|
||||
|
||||
return MasterScaffold(
|
||||
isDesktop: isDesktop,
|
||||
|
@ -172,7 +177,7 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
child: isDesktop
|
||||
? Text(
|
||||
"On the next screen you will see "
|
||||
"${Constants.defaultSeedPhraseLengthFor(coin: coin)} "
|
||||
"$seedCount "
|
||||
"words that make up your recovery phrase.\n\nPlease "
|
||||
"write it down. Keep it safe and never share it with "
|
||||
"anyone. Your recovery phrase is the only way you can"
|
||||
|
@ -216,9 +221,7 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"${Constants.defaultSeedPhraseLengthFor(coin: coin)}"
|
||||
" words",
|
||||
text: "$seedCount words",
|
||||
style: STextStyles.desktopH3(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -496,7 +499,24 @@ class _NewWalletRecoveryPhraseWarningViewState
|
|||
|
||||
final manager = Manager(wallet);
|
||||
|
||||
await manager.initializeNew();
|
||||
if (coin.hasMnemonicPassphraseSupport &&
|
||||
ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state !=
|
||||
null) {
|
||||
await manager.initializeNew((
|
||||
mnemonicPassphrase: ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state!
|
||||
.mnemonicPassphrase,
|
||||
wordCount: ref
|
||||
.read(pNewWalletOptions.state)
|
||||
.state!
|
||||
.mnemonicWordsCount,
|
||||
));
|
||||
} else {
|
||||
await manager.initializeNew(null);
|
||||
}
|
||||
|
||||
// pop progress dialog
|
||||
if (mounted) {
|
||||
|
|
|
@ -535,7 +535,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
|||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: standardInputDecoration(
|
||||
"Recovery phrase password",
|
||||
"BIP39 passphrase",
|
||||
passwordFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
|
@ -586,7 +586,9 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
|
|||
RoundedWhiteContainer(
|
||||
child: Center(
|
||||
child: Text(
|
||||
"If the recovery phrase you are about to restore was created with an optional passphrase you can enter it here.",
|
||||
"If the recovery phrase you are about to restore "
|
||||
"was created with an optional BIP39 passphrase "
|
||||
"you can enter it here.",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
|
|
|
@ -98,7 +98,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
|
||||
final List<TextEditingController> _controllers = [];
|
||||
final List<FormInputStatus> _inputStatuses = [];
|
||||
final List<FocusNode> _focusNodes = [];
|
||||
// final List<FocusNode> _focusNodes = [];
|
||||
|
||||
late final BarcodeScannerInterface scanner;
|
||||
|
||||
|
@ -152,7 +152,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
for (int i = 0; i < _seedWordCount; i++) {
|
||||
_controllers.add(TextEditingController());
|
||||
_inputStatuses.add(FormInputStatus.empty);
|
||||
_focusNodes.add(FocusNode());
|
||||
// _focusNodes.add(FocusNode());
|
||||
}
|
||||
|
||||
super.initState();
|
||||
|
@ -821,8 +821,8 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
i * 4 + j - 1 == 1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
focusNode:
|
||||
_focusNodes[i * 4 + j - 1],
|
||||
// focusNode:
|
||||
// _focusNodes[i * 4 + j - 1],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
@ -841,18 +841,18 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
FormInputStatus.invalid;
|
||||
}
|
||||
|
||||
if (formInputStatus ==
|
||||
FormInputStatus.valid) {
|
||||
if (i * 4 + j <
|
||||
_focusNodes.length) {
|
||||
_focusNodes[i * 4 + j]
|
||||
.requestFocus();
|
||||
} else if (i * 4 + j ==
|
||||
_focusNodes.length) {
|
||||
_focusNodes[i * 4 + j - 1]
|
||||
.unfocus();
|
||||
}
|
||||
}
|
||||
// if (formInputStatus ==
|
||||
// FormInputStatus.valid) {
|
||||
// if (i * 4 + j <
|
||||
// _focusNodes.length) {
|
||||
// _focusNodes[i * 4 + j]
|
||||
// .requestFocus();
|
||||
// } else if (i * 4 + j ==
|
||||
// _focusNodes.length) {
|
||||
// _focusNodes[i * 4 + j - 1]
|
||||
// .unfocus();
|
||||
// }
|
||||
// }
|
||||
setState(() {
|
||||
_inputStatuses[i * 4 +
|
||||
j -
|
||||
|
@ -929,7 +929,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
selectionControls: i == 1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
focusNode: _focusNodes[i],
|
||||
// focusNode: _focusNodes[i],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
@ -948,27 +948,27 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
FormInputStatus.invalid;
|
||||
}
|
||||
|
||||
if (formInputStatus ==
|
||||
FormInputStatus
|
||||
.valid &&
|
||||
(i - 1) <
|
||||
_focusNodes.length) {
|
||||
Focus.of(context)
|
||||
.requestFocus(
|
||||
_focusNodes[i]);
|
||||
}
|
||||
// if (formInputStatus ==
|
||||
// FormInputStatus
|
||||
// .valid &&
|
||||
// (i - 1) <
|
||||
// _focusNodes.length) {
|
||||
// Focus.of(context)
|
||||
// .requestFocus(
|
||||
// _focusNodes[i]);
|
||||
// }
|
||||
|
||||
if (formInputStatus ==
|
||||
FormInputStatus.valid) {
|
||||
if (i + 1 <
|
||||
_focusNodes.length) {
|
||||
_focusNodes[i + 1]
|
||||
.requestFocus();
|
||||
} else if (i + 1 ==
|
||||
_focusNodes.length) {
|
||||
_focusNodes[i].unfocus();
|
||||
}
|
||||
}
|
||||
// if (formInputStatus ==
|
||||
// FormInputStatus.valid) {
|
||||
// if (i + 1 <
|
||||
// _focusNodes.length) {
|
||||
// _focusNodes[i + 1]
|
||||
// .requestFocus();
|
||||
// } else if (i + 1 ==
|
||||
// _focusNodes.length) {
|
||||
// _focusNodes[i].unfocus();
|
||||
// }
|
||||
// }
|
||||
},
|
||||
controller: _controllers[i],
|
||||
style:
|
||||
|
@ -1068,7 +1068,7 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
AutovalidateMode.onUserInteraction,
|
||||
selectionControls:
|
||||
i == 1 ? textSelectionControls : null,
|
||||
focusNode: _focusNodes[i - 1],
|
||||
// focusNode: _focusNodes[i - 1],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus formInputStatus;
|
||||
|
||||
|
@ -1084,14 +1084,14 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
FormInputStatus.invalid;
|
||||
}
|
||||
|
||||
if (formInputStatus ==
|
||||
FormInputStatus.valid) {
|
||||
if (i < _focusNodes.length) {
|
||||
_focusNodes[i].requestFocus();
|
||||
} else if (i == _focusNodes.length) {
|
||||
_focusNodes[i - 1].unfocus();
|
||||
}
|
||||
}
|
||||
// if (formInputStatus ==
|
||||
// FormInputStatus.valid) {
|
||||
// if (i < _focusNodes.length) {
|
||||
// _focusNodes[i].requestFocus();
|
||||
// } else if (i == _focusNodes.length) {
|
||||
// _focusNodes[i - 1].unfocus();
|
||||
// }
|
||||
// }
|
||||
setState(() {
|
||||
_inputStatuses[i - 1] =
|
||||
formInputStatus;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,9 +16,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_mnemonic_passphrase_dialog.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
|
@ -98,8 +100,25 @@ class _VerifyRecoveryPhraseViewState
|
|||
// }
|
||||
// }
|
||||
|
||||
Future<bool> _verifyMnemonicPassphrase() async {
|
||||
final result = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (_) => const VerifyMnemonicPassphraseDialog(),
|
||||
);
|
||||
|
||||
return result == "verified";
|
||||
}
|
||||
|
||||
Future<void> _continue(bool isMatch) async {
|
||||
if (isMatch) {
|
||||
if (ref.read(pNewWalletOptions.state).state != null) {
|
||||
final passphraseVerified = await _verifyMnemonicPassphrase();
|
||||
|
||||
if (!passphraseVerified) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await ref.read(walletsServiceChangeNotifierProvider).setMnemonicVerified(
|
||||
walletId: _manager.walletId,
|
||||
);
|
||||
|
|
|
@ -9,38 +9,82 @@
|
|||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
|
||||
import 'package:stackwallet/pages/buy_view/buy_form.dart';
|
||||
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class BuyView extends StatelessWidget {
|
||||
class BuyView extends ConsumerStatefulWidget {
|
||||
const BuyView({
|
||||
Key? key,
|
||||
this.coin,
|
||||
this.tokenContract,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String routeName = "/stackBuyView";
|
||||
|
||||
final Coin? coin;
|
||||
final EthContract? tokenContract;
|
||||
|
||||
static const String routeName = "/stackBuyView";
|
||||
|
||||
@override
|
||||
ConsumerState<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
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
return Stack(
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: BuyForm(
|
||||
coin: coin,
|
||||
tokenContract: tokenContract,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: BuyForm(
|
||||
coin: coin,
|
||||
tokenContract: tokenContract,
|
||||
),
|
||||
),
|
||||
if (torEnabled)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.extension<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",
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import 'package:stackwallet/themes/stack_colors.dart';
|
|||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
|
@ -39,6 +40,8 @@ import 'package:stackwallet/widgets/stack_dialog.dart';
|
|||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import '../../../services/exchange/exchange.dart';
|
||||
|
||||
class ExchangeCurrencySelectionView extends StatefulWidget {
|
||||
const ExchangeCurrencySelectionView({
|
||||
Key? key,
|
||||
|
@ -125,7 +128,7 @@ class _ExchangeCurrencySelectionViewState
|
|||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => StackDialog(
|
||||
title: "ChangeNOW Error",
|
||||
title: "Exchange Error",
|
||||
message: "Failed to load currency data: ${cn.exception}",
|
||||
leftButton: SecondaryButton(
|
||||
label: "Ok",
|
||||
|
@ -169,6 +172,15 @@ class _ExchangeCurrencySelectionViewState
|
|||
.thenByName()
|
||||
.findAll();
|
||||
|
||||
// If using Tor, filter exchanges which do not support Tor.
|
||||
if (Prefs.instance.useTor) {
|
||||
if (Exchange.exchangeNamesWithTorSupport.isNotEmpty) {
|
||||
currencies.removeWhere((element) => !Exchange
|
||||
.exchangeNamesWithTorSupport
|
||||
.contains(element.exchangeName));
|
||||
}
|
||||
}
|
||||
|
||||
return _getDistinctCurrenciesFrom(currencies);
|
||||
}
|
||||
|
||||
|
|
|
@ -31,15 +31,19 @@ import 'package:stackwallet/pages/exchange_view/sub_widgets/rate_type_toggle.dar
|
|||
import 'package:stackwallet/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange_response.dart';
|
||||
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_unit.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||
|
@ -55,8 +59,6 @@ import 'package:stackwallet/widgets/textfields/exchange_textfield.dart';
|
|||
import 'package:tuple/tuple.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../../services/exchange/exchange_response.dart';
|
||||
|
||||
class ExchangeForm extends ConsumerStatefulWidget {
|
||||
const ExchangeForm({
|
||||
Key? key,
|
||||
|
@ -78,7 +80,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
late final Coin? coin;
|
||||
late final bool walletInitiated;
|
||||
|
||||
final exchanges = [
|
||||
var exchanges = [
|
||||
MajesticBankExchange.instance,
|
||||
ChangeNowExchange.instance,
|
||||
TrocadorExchange.instance,
|
||||
|
@ -773,6 +775,14 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
});
|
||||
}
|
||||
|
||||
// Instantiate the Tor service.
|
||||
torService = TorService.sharedInstance;
|
||||
|
||||
// Filter exchanges based on Tor support.
|
||||
if (Prefs.instance.useTor) {
|
||||
exchanges = Exchange.exchangesWithTorSupport;
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -1007,4 +1017,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
|
|||
],
|
||||
);
|
||||
}
|
||||
|
||||
// TorService instance.
|
||||
late TorService torService;
|
||||
}
|
||||
|
|
|
@ -252,10 +252,17 @@ class _Step4ViewState extends ConsumerState<Step4View> {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
final memo =
|
||||
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
|
||||
? model.trade!.payInExtraId.isNotEmpty
|
||||
? model.trade!.payInExtraId
|
||||
: null
|
||||
: null;
|
||||
txDataFuture = manager.prepareSend(
|
||||
address: address,
|
||||
amount: amount,
|
||||
args: {
|
||||
"memo": memo,
|
||||
"feeRate": FeeRateType.average,
|
||||
// ref.read(feeRateTypeStateProvider)
|
||||
},
|
||||
|
@ -568,6 +575,74 @@ class _Step4ViewState extends ConsumerState<Step4View> {
|
|||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
if (model.trade!.payInExtraId.isNotEmpty)
|
||||
RoundedWhiteContainer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Memo",
|
||||
style:
|
||||
STextStyles.itemSubtitle(context),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final data = ClipboardData(
|
||||
text:
|
||||
model.trade!.payInExtraId);
|
||||
await clipboard.setData(data);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message:
|
||||
"Copied to clipboard",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
color: Theme.of(context)
|
||||
.extension<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(
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
@ -268,10 +268,17 @@ class _SendFromCardState extends ConsumerState<SendFromCard> {
|
|||
|
||||
// if not firo then do normal send
|
||||
if (shouldSendPublicFiroFunds == null) {
|
||||
final memo =
|
||||
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
|
||||
? trade.payInExtraId.isNotEmpty
|
||||
? trade.payInExtraId
|
||||
: null
|
||||
: null;
|
||||
txDataFuture = manager.prepareSend(
|
||||
address: address,
|
||||
amount: amount,
|
||||
args: {
|
||||
"memo": memo,
|
||||
"feeRate": FeeRateType.average,
|
||||
// ref.read(feeRateTypeStateProvider)
|
||||
},
|
||||
|
|
|
@ -14,9 +14,11 @@ import 'package:stackwallet/models/exchange/aggregate_currency.dart';
|
|||
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_provider_option.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart';
|
||||
import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
|
@ -44,6 +46,13 @@ class _ExchangeProviderOptionsState
|
|||
required AggregateCurrency? sendCurrency,
|
||||
required AggregateCurrency? receiveCurrency,
|
||||
}) {
|
||||
// If using Tor, only allow exchanges that support it.
|
||||
if (Prefs.instance.useTor) {
|
||||
if (!Exchange.exchangeNamesWithTorSupport.contains(exchangeName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final send = sendCurrency?.forExchange(exchangeName);
|
||||
if (send == null) return false;
|
||||
|
||||
|
|
|
@ -850,6 +850,81 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
|
|||
: const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx)
|
||||
RoundedWhiteContainer(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(16)
|
||||
: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Memo",
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
isDesktop
|
||||
? IconCopyButton(
|
||||
data: trade.payInExtraId,
|
||||
)
|
||||
: GestureDetector(
|
||||
onTap: () async {
|
||||
final address = trade.payInExtraId;
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: address,
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.copy,
|
||||
width: 12,
|
||||
height: 12,
|
||||
color: Theme.of(context)
|
||||
.extension<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(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(16)
|
||||
|
|
|
@ -24,6 +24,7 @@ import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
|
|||
import 'package:stackwallet/providers/global/notifications_provider.dart';
|
||||
import 'package:stackwallet/providers/ui/home_view_index_provider.dart';
|
||||
import 'package:stackwallet/providers/ui/unread_notifications_provider.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/themes/theme_providers.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
|
@ -32,6 +33,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
|
|||
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/small_tor_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class HomeView extends ConsumerStatefulWidget {
|
||||
|
@ -55,6 +57,8 @@ class _HomeViewState extends ConsumerState<HomeView> {
|
|||
|
||||
bool _exitEnabled = false;
|
||||
|
||||
late TorConnectionStatus _currentSyncStatus;
|
||||
|
||||
// final _buyDataLoadingService = BuyDataLoadingService();
|
||||
|
||||
Future<bool> _onWillPop() async {
|
||||
|
@ -125,6 +129,20 @@ class _HomeViewState extends ConsumerState<HomeView> {
|
|||
|
||||
ref.read(notificationsProvider).startCheckingWatchedNotifications();
|
||||
|
||||
/// todo change to watch tor network
|
||||
// if (ref.read(managerProvider).isRefreshing) {
|
||||
// _currentSyncStatus = WalletSyncStatus.syncing;
|
||||
// _currentNodeStatus = NodeConnectionStatus.connected;
|
||||
// } else {
|
||||
// _currentSyncStatus = WalletSyncStatus.synced;
|
||||
// if (ref.read(managerProvider).isConnected) {
|
||||
// _currentNodeStatus = NodeConnectionStatus.connected;
|
||||
// } else {
|
||||
// _currentNodeStatus = NodeConnectionStatus.disconnected;
|
||||
// _currentSyncStatus = WalletSyncStatus.unableToSync;
|
||||
// }
|
||||
// }
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -200,6 +218,17 @@ class _HomeViewState extends ConsumerState<HomeView> {
|
|||
],
|
||||
),
|
||||
actions: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: SmallTorIcon(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
|
|
|
@ -5,20 +5,22 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
||||
import 'package:stackwallet/models/isar/ordinal.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
|
@ -230,11 +232,16 @@ class _OrdinalImageGroup extends StatelessWidget {
|
|||
static const _spacing = 12.0;
|
||||
|
||||
Future<String> _savePngToFile() async {
|
||||
final response = await get(Uri.parse(ordinal.content));
|
||||
HTTP client = HTTP();
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
"statusCode=${response.statusCode} body=${response.bodyBytes}");
|
||||
final response = await client.get(
|
||||
url: Uri.parse(ordinal.content),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
if (response.code != 200) {
|
||||
throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
|
||||
}
|
||||
|
||||
final bytes = response.bodyBytes;
|
||||
|
|
|
@ -307,14 +307,20 @@ class _ReceiveViewState extends ConsumerState<ReceiveView> {
|
|||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano)
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano)
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
TextButton(
|
||||
onPressed: generateNewAddress,
|
||||
style: Theme.of(context)
|
||||
|
|
|
@ -103,6 +103,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
late TextEditingController noteController;
|
||||
late TextEditingController onChainNoteController;
|
||||
late TextEditingController feeController;
|
||||
late TextEditingController memoController;
|
||||
|
||||
late final SendViewAutoFillData? _data;
|
||||
|
||||
|
@ -111,6 +112,9 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
final _onChainNoteFocusNode = FocusNode();
|
||||
final _cryptoFocus = FocusNode();
|
||||
final _baseFocus = FocusNode();
|
||||
final _memoFocus = FocusNode();
|
||||
|
||||
late final bool isStellar;
|
||||
|
||||
Amount? _amountToSend;
|
||||
Amount? _cachedAmountToSend;
|
||||
|
@ -522,10 +526,15 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
final memo =
|
||||
manager.coin == Coin.stellar || manager.coin == Coin.stellarTestnet
|
||||
? memoController.text
|
||||
: null;
|
||||
txDataFuture = manager.prepareSend(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
args: {
|
||||
"memo": memo,
|
||||
"feeRate": ref.read(feeRateTypeStateProvider),
|
||||
"satsPerVByte": isCustomFee ? customFeeRate : null,
|
||||
"UTXOs": (manager.hasCoinControlSupport &&
|
||||
|
@ -622,6 +631,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
walletId = widget.walletId;
|
||||
clipboard = widget.clipboard;
|
||||
scanner = widget.barcodeScanner;
|
||||
isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
|
||||
|
||||
sendToController = TextEditingController();
|
||||
cryptoAmountController = TextEditingController();
|
||||
|
@ -629,6 +639,7 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
noteController = TextEditingController();
|
||||
onChainNoteController = TextEditingController();
|
||||
feeController = TextEditingController();
|
||||
memoController = TextEditingController();
|
||||
|
||||
onCryptoAmountChanged = _cryptoAmountChanged;
|
||||
cryptoAmountController.addListener(onCryptoAmountChanged);
|
||||
|
@ -704,12 +715,14 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
noteController.dispose();
|
||||
onChainNoteController.dispose();
|
||||
feeController.dispose();
|
||||
memoController.dispose();
|
||||
|
||||
_noteFocusNode.dispose();
|
||||
_onChainNoteFocusNode.dispose();
|
||||
_addressFocusNode.dispose();
|
||||
_cryptoFocus.dispose();
|
||||
_baseFocus.dispose();
|
||||
_memoFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -1298,6 +1311,88 @@ class _SendViewState extends ConsumerState<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: (_) {
|
||||
final error = _updateInvalidAddressText(
|
||||
|
@ -1817,7 +1912,8 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
),
|
||||
child: TextField(
|
||||
autocorrect: Util.isDesktop ? false : true,
|
||||
enableSuggestions: Util.isDesktop ? false : true,
|
||||
enableSuggestions:
|
||||
Util.isDesktop ? false : true,
|
||||
maxLength: 256,
|
||||
controller: onChainNoteController,
|
||||
focusNode: _onChainNoteFocusNode,
|
||||
|
@ -1828,25 +1924,27 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
_onChainNoteFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
suffixIcon: onChainNoteController.text.isNotEmpty
|
||||
suffixIcon: onChainNoteController
|
||||
.text.isNotEmpty
|
||||
? Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
TextFieldIconButton(
|
||||
child: const XIcon(),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
onChainNoteController.text = "";
|
||||
});
|
||||
},
|
||||
padding:
|
||||
const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
TextFieldIconButton(
|
||||
child: const XIcon(),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
onChainNoteController
|
||||
.text = "";
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
@ -1856,8 +1954,9 @@ class _SendViewState extends ConsumerState<SendView> {
|
|||
height: 12,
|
||||
),
|
||||
Text(
|
||||
(coin == Coin.epicCash) ? "Local Note (optional)"
|
||||
: "Note (optional)",
|
||||
(coin == Coin.epicCash)
|
||||
? "Local Note (optional)"
|
||||
: "Note (optional)",
|
||||
style: STextStyles.smallMed12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
|
|
|
@ -15,11 +15,13 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_libepiccash/git_versions.dart' as EPIC_VERSIONS;
|
||||
import 'package:flutter_libmonero/git_versions.dart' as MONERO_VERSIONS;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
|
@ -39,14 +41,17 @@ Future<bool> doesCommitExist(
|
|||
String commit,
|
||||
) async {
|
||||
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||
final Client client = Client();
|
||||
// final Client client = Client();
|
||||
HTTP client = HTTP();
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
"$kGithubAPI$kGithubHead/$organization/$project/commits/$commit");
|
||||
|
||||
final commitQuery = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final response = jsonDecode(commitQuery.body.toString());
|
||||
|
@ -76,14 +81,16 @@ Future<bool> isHeadCommit(
|
|||
String commit,
|
||||
) async {
|
||||
Logging.instance.log("doesCommitExist", level: LogLevel.Info);
|
||||
final Client client = Client();
|
||||
HTTP client = HTTP();
|
||||
try {
|
||||
final uri = Uri.parse(
|
||||
"$kGithubAPI$kGithubHead/$organization/$project/commits/$branch");
|
||||
|
||||
final commitQuery = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final response = jsonDecode(commitQuery.body.toString());
|
||||
|
|
|
@ -25,6 +25,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/stack_back
|
|||
import 'package:stackwallet/pages/settings_views/global_settings_view/startup_preferences/startup_preferences_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/support_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
|
||||
import 'package:stackwallet/route_generator.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
|
@ -159,6 +160,18 @@ class GlobalSettingsView extends StatelessWidget {
|
|||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
SettingsListButton(
|
||||
iconAssetName: Assets.svg.tor,
|
||||
iconSize: 18,
|
||||
title: "Tor Settings",
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(TorSettingsView.routeName);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
SettingsListButton(
|
||||
iconAssetName: Assets.svg.node,
|
||||
iconSize: 16,
|
||||
|
|
|
@ -167,77 +167,77 @@ class HiddenSettings extends StatelessWidget {
|
|||
// ),
|
||||
// );
|
||||
// }),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Consumer(builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
ref
|
||||
.read(priceAnd24hChangeNotifierProvider)
|
||||
.tokenContractAddressesToCheck
|
||||
.add(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
ref
|
||||
.read(priceAnd24hChangeNotifierProvider)
|
||||
.tokenContractAddressesToCheck
|
||||
.add(
|
||||
"0xdAC17F958D2ee523a2206206994597C13D831ec7");
|
||||
await ref
|
||||
.read(priceAnd24hChangeNotifierProvider)
|
||||
.updatePrice();
|
||||
|
||||
final x = ref
|
||||
.read(priceAnd24hChangeNotifierProvider)
|
||||
.getTokenPrice(
|
||||
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
|
||||
print(
|
||||
"PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x");
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Click me",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Consumer(builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
// final erc20 = Erc20ContractInfo(
|
||||
// contractAddress: 'some con',
|
||||
// name: "loonamsn",
|
||||
// symbol: "DD",
|
||||
// decimals: 19,
|
||||
// );
|
||||
//
|
||||
// final json = erc20.toJson();
|
||||
//
|
||||
// print(json);
|
||||
//
|
||||
// final ee = EthContractInfo.fromJson(json);
|
||||
//
|
||||
// print(ee);
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Click me",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
// const SizedBox(
|
||||
// height: 12,
|
||||
// ),
|
||||
// Consumer(builder: (_, ref, __) {
|
||||
// return GestureDetector(
|
||||
// onTap: () async {
|
||||
// ref
|
||||
// .read(priceAnd24hChangeNotifierProvider)
|
||||
// .tokenContractAddressesToCheck
|
||||
// .add(
|
||||
// "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
// ref
|
||||
// .read(priceAnd24hChangeNotifierProvider)
|
||||
// .tokenContractAddressesToCheck
|
||||
// .add(
|
||||
// "0xdAC17F958D2ee523a2206206994597C13D831ec7");
|
||||
// await ref
|
||||
// .read(priceAnd24hChangeNotifierProvider)
|
||||
// .updatePrice();
|
||||
//
|
||||
// final x = ref
|
||||
// .read(priceAnd24hChangeNotifierProvider)
|
||||
// .getTokenPrice(
|
||||
// "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
|
||||
//
|
||||
// print(
|
||||
// "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x");
|
||||
// },
|
||||
// child: RoundedWhiteContainer(
|
||||
// child: Text(
|
||||
// "Click me",
|
||||
// style: STextStyles.button(context).copyWith(
|
||||
// color: Theme.of(context)
|
||||
// .extension<StackColors>()!
|
||||
// .accentColorDark),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// const SizedBox(
|
||||
// height: 12,
|
||||
// ),
|
||||
// Consumer(builder: (_, ref, __) {
|
||||
// return GestureDetector(
|
||||
// onTap: () async {
|
||||
// // final erc20 = Erc20ContractInfo(
|
||||
// // contractAddress: 'some con',
|
||||
// // name: "loonamsn",
|
||||
// // symbol: "DD",
|
||||
// // decimals: 19,
|
||||
// // );
|
||||
// //
|
||||
// // final json = erc20.toJson();
|
||||
// //
|
||||
// // print(json);
|
||||
// //
|
||||
// // final ee = EthContractInfo.fromJson(json);
|
||||
// //
|
||||
// // print(ee);
|
||||
// },
|
||||
// child: RoundedWhiteContainer(
|
||||
// child: Text(
|
||||
// "Click me",
|
||||
// style: STextStyles.button(context).copyWith(
|
||||
// color: Theme.of(context)
|
||||
// .extension<StackColors>()!
|
||||
// .accentColorDark),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
|
|
|
@ -27,6 +27,7 @@ import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
|||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
|
||||
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
|
||||
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
|
@ -193,13 +194,20 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
try {
|
||||
// await client.getSyncStatus();
|
||||
} catch (_) {}
|
||||
break;
|
||||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
try {
|
||||
testPassed =
|
||||
await testStellarNodeConnection(formData.host!, formData.port!);
|
||||
} catch (_) {}
|
||||
break;
|
||||
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
case Coin.tezos:
|
||||
throw UnimplementedError();
|
||||
//TODO: check network/node
|
||||
//TODO: check network/node
|
||||
}
|
||||
|
||||
if (showFlushBar && mounted) {
|
||||
|
@ -730,6 +738,7 @@ class _NodeFormState extends ConsumerState<NodeForm> {
|
|||
case Coin.namecoin:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.particl:
|
||||
case Coin.tezos:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
|
|
|
@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/logger.dart';
|
|||
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
|
||||
import 'package:stackwallet/utilities/test_eth_node_connection.dart';
|
||||
import 'package:stackwallet/utilities/test_monero_node_connection.dart';
|
||||
import 'package:stackwallet/utilities/test_stellar_node_connection.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
|
@ -172,10 +173,17 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
|
|||
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.tezos:
|
||||
throw UnimplementedError();
|
||||
//TODO: check network/node
|
||||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
throw UnimplementedError();
|
||||
//TODO: check network/node
|
||||
try {
|
||||
testPassed = await testStellarNodeConnection(node!.host, node.port);
|
||||
} catch (_) {
|
||||
testPassed = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (testPassed) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -27,12 +27,15 @@ import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
|
|||
import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/animated_text.dart';
|
||||
|
@ -92,6 +95,13 @@ class _WalletNetworkSettingsViewState
|
|||
late int _blocksRemaining;
|
||||
bool _advancedIsExpanded = false;
|
||||
|
||||
/// The current status of the Tor connection.
|
||||
late TorConnectionStatus _torConnectionStatus;
|
||||
|
||||
/// The subscription to the TorConnectionStatusChangedEvent.
|
||||
late final StreamSubscription<TorConnectionStatusChangedEvent>
|
||||
_torConnectionStatusSubscription;
|
||||
|
||||
Future<void> _attemptRescan() async {
|
||||
if (!Platform.isLinux) await Wakelock.enable();
|
||||
|
||||
|
@ -268,6 +278,25 @@ class _WalletNetworkSettingsViewState
|
|||
// }
|
||||
// },
|
||||
// );
|
||||
|
||||
// Initialize the TorConnectionStatus.
|
||||
_torConnectionStatus = ref.read(pTorService).enabled
|
||||
? TorConnectionStatus.connected
|
||||
: TorConnectionStatus.disconnected;
|
||||
|
||||
// Subscribe to the TorConnectionStatusChangedEvent.
|
||||
_torConnectionStatusSubscription =
|
||||
eventBus.on<TorConnectionStatusChangedEvent>().listen(
|
||||
(event) async {
|
||||
// Rebuild the widget.
|
||||
setState(() {
|
||||
_torConnectionStatus = event.newStatus;
|
||||
});
|
||||
|
||||
// TODO implement spinner or animations and control from here
|
||||
},
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -277,6 +306,7 @@ class _WalletNetworkSettingsViewState
|
|||
_syncStatusSubscription.cancel();
|
||||
_refreshSubscription.cancel();
|
||||
_blocksRemainingSubscription?.cancel();
|
||||
_torConnectionStatusSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -340,92 +370,98 @@ class _WalletNetworkSettingsViewState
|
|||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
key: const Key(
|
||||
"walletNetworkSettingsAddNewNodeViewButton"),
|
||||
size: 36,
|
||||
shadows: const [],
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.verticalEllipsis,
|
||||
if (ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.coin !=
|
||||
Coin.epicCash)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
key: const Key(
|
||||
"walletNetworkSettingsAddNewNodeViewButton"),
|
||||
size: 36,
|
||||
shadows: const [],
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog<dynamic>(
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 9,
|
||||
right: 10,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius),
|
||||
// boxShadow: [CFColors.standardBoxShadow],
|
||||
boxShadow: const [],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return ConfirmFullRescanDialog(
|
||||
onConfirm: _attemptRescan,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
"Rescan blockchain",
|
||||
style:
|
||||
STextStyles.baseXS(context),
|
||||
.background,
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.verticalEllipsis,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog<dynamic>(
|
||||
barrierColor: Colors.transparent,
|
||||
barrierDismissible: true,
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
top: 9,
|
||||
right: 10,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants
|
||||
.size.circularBorderRadius),
|
||||
// boxShadow: [CFColors.standardBoxShadow],
|
||||
boxShadow: const [],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return ConfirmFullRescanDialog(
|
||||
onConfirm: _attemptRescan,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Text(
|
||||
"Rescan blockchain",
|
||||
style: STextStyles.baseXS(
|
||||
context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
|
@ -521,14 +557,6 @@ class _WalletNetworkSettingsViewState
|
|||
"Synchronized",
|
||||
style: STextStyles.w600_12(context),
|
||||
),
|
||||
Text(
|
||||
"100%",
|
||||
style: STextStyles.syncPercent(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -749,6 +777,161 @@ class _WalletNetworkSettingsViewState
|
|||
SizedBox(
|
||||
height: isDesktop ? 32 : 20,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Tor status",
|
||||
textAlign: TextAlign.left,
|
||||
style: isDesktop
|
||||
? STextStyles.desktopTextExtraExtraSmall(context)
|
||||
: STextStyles.smallMed12(context),
|
||||
),
|
||||
if (ref.watch(
|
||||
prefsChangeNotifierProvider.select((value) => value.useTor)))
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Stop the Tor service.
|
||||
try {
|
||||
await ref.read(pTorService).stop();
|
||||
|
||||
// Toggle the useTor preference on success.
|
||||
ref.read(prefsChangeNotifierProvider).useTor = false;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error stopping tor: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
"Disconnect",
|
||||
style: STextStyles.link2(context),
|
||||
),
|
||||
),
|
||||
if (!ref.watch(
|
||||
prefsChangeNotifierProvider.select((value) => value.useTor)))
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Init the Tor service if it hasn't already been.
|
||||
ref.read(pTorService).init();
|
||||
|
||||
// Start the Tor service.
|
||||
try {
|
||||
await ref.read(pTorService).start();
|
||||
|
||||
// Toggle the useTor preference on success.
|
||||
ref.read(prefsChangeNotifierProvider).useTor = true;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error starting tor: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
"Connect",
|
||||
style: STextStyles.link2(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 12 : 9,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
borderColor: isDesktop
|
||||
? Theme.of(context).extension<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(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -786,11 +969,21 @@ class _WalletNetworkSettingsViewState
|
|||
.select((value) => value.getManager(widget.walletId).coin)),
|
||||
popBackToRoute: WalletNetworkSettingsView.routeName,
|
||||
),
|
||||
if (isDesktop)
|
||||
if (isDesktop &&
|
||||
ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.coin !=
|
||||
Coin.epicCash)
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
if (isDesktop)
|
||||
if (isDesktop &&
|
||||
ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.coin !=
|
||||
Coin.epicCash)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 12,
|
||||
|
@ -806,7 +999,12 @@ class _WalletNetworkSettingsViewState
|
|||
],
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
if (isDesktop &&
|
||||
ref
|
||||
.read(walletsChangeNotifierProvider)
|
||||
.getManager(widget.walletId)
|
||||
.coin !=
|
||||
Coin.epicCash)
|
||||
RoundedWhiteContainer(
|
||||
borderColor: isDesktop
|
||||
? Theme.of(context).extension<StackColors>()!.background
|
||||
|
|
|
@ -13,7 +13,6 @@ import 'dart:async';
|
|||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/models/epicbox_config_model.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
|
||||
|
@ -37,14 +36,11 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart';
|
|||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// [eventBus] should only be set during testing
|
||||
|
@ -310,61 +306,6 @@ class _WalletSettingsViewState extends ConsumerState<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)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
|
|
|
@ -35,7 +35,6 @@ class TxIcon extends ConsumerWidget {
|
|||
|
||||
String _getAssetName(
|
||||
bool isCancelled, bool isReceived, bool isPending, IThemeAssets assets) {
|
||||
|
||||
if (!isReceived && transaction.subType == TransactionSubType.mint) {
|
||||
if (isCancelled) {
|
||||
return Assets.svg.anonymizeFailed;
|
||||
|
@ -48,7 +47,7 @@ class TxIcon extends ConsumerWidget {
|
|||
|
||||
if (isReceived) {
|
||||
if (isCancelled) {
|
||||
return assets.receiveCancelled;
|
||||
return assets.receive;
|
||||
}
|
||||
if (isPending) {
|
||||
return assets.receivePending;
|
||||
|
|
|
@ -358,6 +358,8 @@ class _TransactionDetailsViewState
|
|||
final currentHeight = ref.watch(walletsChangeNotifierProvider
|
||||
.select((value) => value.getManager(walletId).currentHeight));
|
||||
|
||||
print("THIS TRANSACTION IS $_transaction");
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !isDesktop,
|
||||
builder: (child) => Background(
|
||||
|
@ -1577,11 +1579,7 @@ class _TransactionDetailsViewState
|
|||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: (coin == Coin.epicCash &&
|
||||
_transaction.isConfirmed(
|
||||
currentHeight,
|
||||
coin.requiredConfirmations,
|
||||
) ==
|
||||
false &&
|
||||
_transaction.getConfirmations(currentHeight) < 1 &&
|
||||
_transaction.isCancelled == false)
|
||||
? ConditionalParent(
|
||||
condition: isDesktop,
|
||||
|
|
|
@ -72,6 +72,7 @@ import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
|||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||
import 'package:stackwallet/widgets/small_tor_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
|
||||
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart';
|
||||
|
@ -555,6 +556,17 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
],
|
||||
),
|
||||
actions: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: SmallTorIcon(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
|
|
|
@ -9,76 +9,137 @@
|
|||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/buy_view/buy_form.dart';
|
||||
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class DesktopBuyView extends StatefulWidget {
|
||||
class DesktopBuyView extends ConsumerStatefulWidget {
|
||||
const DesktopBuyView({Key? key}) : super(key: key);
|
||||
|
||||
static const String routeName = "/desktopBuyView";
|
||||
|
||||
@override
|
||||
State<DesktopBuyView> createState() => _DesktopBuyViewState();
|
||||
ConsumerState<DesktopBuyView> createState() => _DesktopBuyViewState();
|
||||
}
|
||||
|
||||
class _DesktopBuyViewState extends State<DesktopBuyView> {
|
||||
class _DesktopBuyViewState extends ConsumerState<DesktopBuyView> {
|
||||
late bool torEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
setState(() {
|
||||
torEnabled = ref.read(prefsChangeNotifierProvider).useTor;
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopScaffold(
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
),
|
||||
child: Text(
|
||||
"Buy crypto",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: BuyForm(),
|
||||
),
|
||||
],
|
||||
return Stack(
|
||||
children: [
|
||||
DesktopScaffold(
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: true,
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
),
|
||||
child: Text(
|
||||
"Buy crypto",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
bottom: 24,
|
||||
),
|
||||
// Expanded(
|
||||
// child: Row(
|
||||
// children: const [
|
||||
// Expanded(
|
||||
// child: DesktopTradeHistory(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
RoundedWhiteContainer(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: BuyForm(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
// Expanded(
|
||||
// child: Row(
|
||||
// children: const [
|
||||
// Expanded(
|
||||
// child: DesktopTradeHistory(),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (torEnabled)
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.extension<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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,6 +155,23 @@ class _DesktopStep4State extends ConsumerState<DesktopStep4> {
|
|||
height: 1,
|
||||
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(
|
||||
label: "Amount",
|
||||
value:
|
||||
|
|
|
@ -15,10 +15,12 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_menu_item.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu.dart';
|
||||
import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_tor_status_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/living_stack_icon.dart';
|
||||
|
||||
enum DesktopMenuItemId {
|
||||
|
@ -52,11 +54,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
|
|||
|
||||
final Duration duration = const Duration(milliseconds: 250);
|
||||
late final List<DMIController> controllers;
|
||||
late final DMIController torButtonController;
|
||||
|
||||
double _width = expandedWidth;
|
||||
|
||||
// final _buyDataLoadingService = BuyDataLoadingService();
|
||||
|
||||
void updateSelectedMenuItem(DesktopMenuItemId idKey) {
|
||||
widget.onSelectionWillChange?.call(idKey);
|
||||
|
||||
|
@ -72,6 +73,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
|
|||
e.toggle?.call();
|
||||
}
|
||||
|
||||
torButtonController.toggle?.call();
|
||||
|
||||
setState(() {
|
||||
_width = expanded ? minimizedWidth : expandedWidth;
|
||||
});
|
||||
|
@ -91,6 +94,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
|
|||
DMIController(),
|
||||
];
|
||||
|
||||
torButtonController = DMIController();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -99,6 +104,8 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
|
|||
for (var e in controllers) {
|
||||
e.dispose();
|
||||
}
|
||||
torButtonController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -140,7 +147,26 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> {
|
|||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 60,
|
||||
height: 5,
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: duration,
|
||||
width: _width == expandedWidth
|
||||
? _width - 32 // 16 padding on either side
|
||||
: _width - 16, // 8 padding on either side
|
||||
child: DesktopTorStatusButton(
|
||||
transitionDuration: duration,
|
||||
controller: torButtonController,
|
||||
onPressed: () {
|
||||
ref.read(currentDesktopMenuItemProvider.state).state =
|
||||
DesktopMenuItemId.settings;
|
||||
ref.watch(selectedSettingsMenuItemStateProvider.state).state =
|
||||
4;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
Expanded(
|
||||
child: AnimatedContainer(
|
||||
|
|
|
@ -219,14 +219,20 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
|
|||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano)
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
if (coin != Coin.epicCash &&
|
||||
coin != Coin.ethereum &&
|
||||
coin != Coin.banano &&
|
||||
coin != Coin.nano)
|
||||
coin != Coin.nano &&
|
||||
coin != Coin.stellar &&
|
||||
coin != Coin.stellarTestnet &&
|
||||
coin != Coin.tezos)
|
||||
SecondaryButton(
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: generateNewAddress,
|
||||
|
|
|
@ -97,12 +97,16 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
late TextEditingController cryptoAmountController;
|
||||
late TextEditingController baseAmountController;
|
||||
// late TextEditingController feeController;
|
||||
late TextEditingController memoController;
|
||||
|
||||
late final SendViewAutoFillData? _data;
|
||||
|
||||
final _addressFocusNode = FocusNode();
|
||||
final _cryptoFocus = FocusNode();
|
||||
final _baseFocus = FocusNode();
|
||||
final _memoFocus = FocusNode();
|
||||
|
||||
late final bool isStellar;
|
||||
|
||||
String? _note;
|
||||
String? _onChainNote;
|
||||
|
@ -326,10 +330,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
final memo = isStellar ? memoController.text : null;
|
||||
txDataFuture = manager.prepareSend(
|
||||
address: _address!,
|
||||
amount: amount,
|
||||
args: {
|
||||
"memo": memo,
|
||||
"feeRate": ref.read(feeRateTypeStateProvider),
|
||||
"satsPerVByte": isCustomFee ? customFeeRate : null,
|
||||
"UTXOs": (manager.hasCoinControlSupport &&
|
||||
|
@ -663,6 +669,23 @@ class _DesktopSendState extends ConsumerState<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) {
|
||||
final baseAmount = Amount.tryParseFiatString(
|
||||
baseAmountString,
|
||||
|
@ -762,10 +785,12 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
|
||||
clipboard = widget.clipboard;
|
||||
scanner = widget.barcodeScanner;
|
||||
isStellar = coin == Coin.stellar || coin == Coin.stellarTestnet;
|
||||
|
||||
sendToController = TextEditingController();
|
||||
cryptoAmountController = TextEditingController();
|
||||
baseAmountController = TextEditingController();
|
||||
memoController = TextEditingController();
|
||||
// feeController = TextEditingController();
|
||||
|
||||
onCryptoAmountChanged = _cryptoAmountChanged;
|
||||
|
@ -814,11 +839,13 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
sendToController.dispose();
|
||||
cryptoAmountController.dispose();
|
||||
baseAmountController.dispose();
|
||||
memoController.dispose();
|
||||
// feeController.dispose();
|
||||
|
||||
_addressFocusNode.dispose();
|
||||
_cryptoFocus.dispose();
|
||||
_baseFocus.dispose();
|
||||
_memoFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -1367,6 +1394,67 @@ class _DesktopSendState extends ConsumerState<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)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
|
|
|
@ -3,21 +3,23 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
||||
import 'package:stackwallet/models/isar/ordinal.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
|
@ -50,11 +52,16 @@ class _DesktopOrdinalDetailsViewState
|
|||
late final UTXO? utxo;
|
||||
|
||||
Future<String> _savePngToFile() async {
|
||||
final response = await get(Uri.parse(widget.ordinal.content));
|
||||
HTTP client = HTTP();
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
"statusCode=${response.statusCode} body=${response.bodyBytes}");
|
||||
final response = await client.get(
|
||||
url: Uri.parse(widget.ordinal.content),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
if (response.code != 200) {
|
||||
throw Exception("statusCode=${response.code} body=${response.bodyBytes}");
|
||||
}
|
||||
|
||||
final bytes = response.bodyBytes;
|
||||
|
|
|
@ -19,6 +19,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/langua
|
|||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart';
|
||||
import 'package:stackwallet/route_generator.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
|
@ -56,7 +57,12 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
|
|||
key: Key("settingsLanguageDesktopKey"),
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
initialRoute: LanguageOptionSettings.routeName,
|
||||
), //language
|
||||
),
|
||||
const Navigator(
|
||||
key: Key("settingsTorDesktopKey"),
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
initialRoute: TorSettings.routeName,
|
||||
), //tor
|
||||
const Navigator(
|
||||
key: Key("settingsNodesDesktopKey"),
|
||||
onGenerateRoute: RouteGenerator.generateRoute,
|
||||
|
|
|
@ -32,6 +32,7 @@ class _SettingsMenuState extends ConsumerState<SettingsMenu> {
|
|||
"Security",
|
||||
"Currency",
|
||||
"Language",
|
||||
"Tor settings",
|
||||
"Nodes",
|
||||
"Syncing preferences",
|
||||
"Appearance",
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
4
lib/providers/global/http_provider.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
|
||||
final pHTTP = Provider((ref) => HTTP());
|
|
@ -27,6 +27,7 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_to
|
|||
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_options/new_wallet_options_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart';
|
||||
|
@ -110,6 +111,7 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi
|
|||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/tor_settings/tor_settings_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
|
||||
|
@ -167,6 +169,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/langua
|
|||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/nodes_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/security_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/tor_settings/tor_settings.dart';
|
||||
import 'package:stackwallet/services/coins/manager.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
|
||||
|
@ -683,6 +686,18 @@ class RouteGenerator {
|
|||
builder: (_) => const LanguageSettingsView(),
|
||||
settings: RouteSettings(name: settings.name));
|
||||
|
||||
case TorSettingsView.routeName:
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => const TorSettingsView(),
|
||||
settings: RouteSettings(name: settings.name));
|
||||
|
||||
case TorSettings.routeName:
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => const TorSettings(),
|
||||
settings: RouteSettings(name: settings.name));
|
||||
|
||||
case AboutView.routeName:
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
|
@ -1131,6 +1146,21 @@ class RouteGenerator {
|
|||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case NewWalletOptionsView.routeName:
|
||||
if (args is Tuple2<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:
|
||||
if (args is Tuple5<String, Coin, int, DateTime, String>) {
|
||||
return getRoute(
|
||||
|
|
|
@ -12,12 +12,13 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
|
||||
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
|
||||
import 'package:stackwallet/models/buy/response_objects/order.dart';
|
||||
import 'package:stackwallet/models/buy/response_objects/quote.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/buy/buy_response.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/fiat_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
|
@ -35,8 +36,7 @@ class SimplexAPI {
|
|||
static final SimplexAPI _instance = SimplexAPI._();
|
||||
static SimplexAPI get instance => _instance;
|
||||
|
||||
/// set this to override using standard http client. Useful for testing
|
||||
http.Client? client;
|
||||
HTTP client = HTTP();
|
||||
|
||||
Uri _buildUri(String path, Map<String, String>? params) {
|
||||
if (scheme == "http") {
|
||||
|
@ -55,10 +55,15 @@ class SimplexAPI {
|
|||
};
|
||||
Uri url = _buildUri('api.php', data);
|
||||
|
||||
var res = await http.post(url, headers: headers);
|
||||
if (res.statusCode != 200) {
|
||||
var res = await client.post(
|
||||
url: url,
|
||||
headers: headers,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (res.code != 200) {
|
||||
throw Exception(
|
||||
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
|
||||
'getAvailableCurrencies exception: statusCode= ${res.code}');
|
||||
}
|
||||
final jsonArray = jsonDecode(res.body); // TODO handle if invalid json
|
||||
|
||||
|
@ -116,10 +121,15 @@ class SimplexAPI {
|
|||
};
|
||||
Uri url = _buildUri('api.php', data);
|
||||
|
||||
var res = await http.post(url, headers: headers);
|
||||
if (res.statusCode != 200) {
|
||||
var res = await client.post(
|
||||
url: url,
|
||||
headers: headers,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (res.code != 200) {
|
||||
throw Exception(
|
||||
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
|
||||
'getAvailableCurrencies exception: statusCode= ${res.code}');
|
||||
}
|
||||
final jsonArray = jsonDecode(res.body); // TODO validate json
|
||||
|
||||
|
@ -192,9 +202,14 @@ class SimplexAPI {
|
|||
}
|
||||
Uri url = _buildUri('api.php', data);
|
||||
|
||||
var res = await http.get(url, headers: headers);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('getQuote exception: statusCode= ${res.statusCode}');
|
||||
var res = await client.get(
|
||||
url: url,
|
||||
headers: headers,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (res.code != 200) {
|
||||
throw Exception('getQuote exception: statusCode= ${res.code}');
|
||||
}
|
||||
final jsonArray = jsonDecode(res.body);
|
||||
if (jsonArray.containsKey('error') as bool) {
|
||||
|
@ -294,9 +309,14 @@ class SimplexAPI {
|
|||
}
|
||||
Uri url = _buildUri('api.php', data);
|
||||
|
||||
var res = await http.get(url, headers: headers);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('newOrder exception: statusCode= ${res.statusCode}');
|
||||
var res = await client.get(
|
||||
url: url,
|
||||
headers: headers,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (res.code != 200) {
|
||||
throw Exception('newOrder exception: statusCode= ${res.code}');
|
||||
}
|
||||
final jsonArray = jsonDecode(res.body); // TODO check if valid json
|
||||
if (jsonArray.containsKey('error') as bool) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
|
@ -10,6 +9,7 @@ import 'package:stackwallet/models/balance.dart';
|
|||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||
import 'package:stackwallet/models/node_model.dart';
|
||||
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/coins/coin_service.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
|
||||
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
|
||||
|
@ -19,6 +19,7 @@ import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
|||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||
import 'package:stackwallet/services/nano_api.dart';
|
||||
import 'package:stackwallet/services/node_service.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
|
@ -145,10 +146,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Balance get balance => _balance ??= getCachedBalance();
|
||||
Balance? _balance;
|
||||
|
||||
HTTP client = HTTP();
|
||||
|
||||
Future<String?> requestWork(String hash) async {
|
||||
return http
|
||||
return client
|
||||
.post(
|
||||
Uri.parse("https://rpc.nano.to"), // this should be a
|
||||
url: Uri.parse("https://rpc.nano.to"), // this should be a
|
||||
headers: {'Content-type': 'application/json'},
|
||||
body: json.encode(
|
||||
{
|
||||
|
@ -156,17 +159,19 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"hash": hash,
|
||||
},
|
||||
),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
)
|
||||
.then((http.Response response) {
|
||||
if (response.statusCode == 200) {
|
||||
.then((client) {
|
||||
if (client.code == 200) {
|
||||
final Map<String, dynamic> decoded =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
json.decode(client.body) as Map<String, dynamic>;
|
||||
if (decoded.containsKey("error")) {
|
||||
throw Exception("Received error ${decoded["error"]}");
|
||||
}
|
||||
return decoded["work"] as String?;
|
||||
} else {
|
||||
throw Exception("Received error ${response.statusCode}");
|
||||
throw Exception("Received error ${client.code}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -185,10 +190,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final balanceResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final balanceResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
|
||||
|
@ -203,10 +210,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"representative": "true",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final String frontier =
|
||||
|
@ -256,10 +265,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"subtype": "send",
|
||||
"block": sendBlock,
|
||||
});
|
||||
final processResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final processResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> decoded =
|
||||
|
@ -328,8 +339,13 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: headers, body: body);
|
||||
final response = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: body,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final data = jsonDecode(response.body);
|
||||
_balance = Balance(
|
||||
total: Amount(
|
||||
|
@ -367,10 +383,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"representative": "true",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
|
@ -385,10 +403,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"account": publicAddress,
|
||||
});
|
||||
|
||||
final balanceResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final balanceResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
|
@ -458,10 +478,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"subtype": "receive",
|
||||
"block": receiveBlock,
|
||||
});
|
||||
final processResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final processResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> decoded =
|
||||
|
@ -472,14 +494,18 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
Future<void> confirmAllReceivable() async {
|
||||
final receivableResponse = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": await currentReceivingAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
final receivableResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": await currentReceivingAddress,
|
||||
"count": "-1",
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final receivableData = await jsonDecode(receivableResponse.body);
|
||||
if (receivableData["blocks"] == "") {
|
||||
|
@ -501,13 +527,17 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
await confirmAllReceivable();
|
||||
final receivingAddress = (await _currentReceivingAddress)!;
|
||||
final String publicAddress = receivingAddress.value;
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": publicAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
final response = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": publicAddress,
|
||||
"count": "-1",
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final data = await jsonDecode(response.body);
|
||||
final transactions =
|
||||
data["history"] is List ? data["history"] as List<dynamic> : [];
|
||||
|
@ -600,7 +630,9 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
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!");
|
||||
|
@ -817,17 +849,19 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Future<bool> testNetworkConnection() async {
|
||||
final uri = Uri.parse(getCurrentNode().host);
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
final response = await client.post(
|
||||
url: uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode(
|
||||
{
|
||||
"action": "version",
|
||||
},
|
||||
),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
return response.statusCode == 200;
|
||||
return response.code == 200;
|
||||
}
|
||||
|
||||
Timer? _networkAliveTimer;
|
||||
|
@ -913,10 +947,12 @@ class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
|
|
|
@ -1290,7 +1290,9 @@ class BitcoinWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1301,7 +1303,7 @@ class BitcoinWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1533,10 +1537,21 @@ class BitcoinWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: "");
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
final initialAddresses = await Future.wait([
|
||||
|
|
|
@ -1186,7 +1186,9 @@ class BitcoinCashWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1196,7 +1198,7 @@ class BitcoinCashWallet extends CoinServiceAPI
|
|||
}
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1459,10 +1463,21 @@ class BitcoinCashWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: "");
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
final initialAddresses = await Future.wait([
|
||||
|
|
|
@ -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/particl/particl_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/transaction_notification_tracker.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
|
@ -228,6 +229,24 @@ abstract class CoinServiceAPI {
|
|||
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:
|
||||
return WowneroWallet(
|
||||
walletId: walletId,
|
||||
|
@ -285,15 +304,6 @@ abstract class CoinServiceAPI {
|
|||
cachedClient: cachedClient,
|
||||
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,
|
||||
});
|
||||
|
||||
Future<void> initializeNew();
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
);
|
||||
Future<void> initializeExisting();
|
||||
|
||||
Future<void> exit();
|
||||
|
|
|
@ -1147,7 +1147,9 @@ class DogecoinWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1157,7 +1159,7 @@ class DogecoinWallet extends CoinServiceAPI
|
|||
}
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1383,12 +1387,20 @@ class DogecoinWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses
|
||||
|
|
|
@ -506,7 +506,9 @@ class ECashWallet extends CoinServiceAPI
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _generateNewWallet() async {
|
||||
Future<void> _generateNewWallet(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -544,10 +546,21 @@ class ECashWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
await _secureStore.write(key: '${_walletId}_mnemonicPassphrase', value: "");
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
const int startingIndex = 0;
|
||||
const int receiveChain = 0;
|
||||
|
@ -2778,7 +2791,9 @@ class ECashWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -2789,7 +2804,7 @@ class ECashWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
level: LogLevel.Fatal);
|
||||
|
|
|
@ -176,7 +176,7 @@ Future<void> executeNative(Map<String, dynamic> arguments) async {
|
|||
final selectionStrategyIsAll =
|
||||
arguments['selectionStrategyIsAll'] 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 address = arguments['address'] as String?;
|
||||
|
||||
|
@ -421,34 +421,31 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
|
||||
late SecureStorageInterface _secureStore;
|
||||
|
||||
/// returns an empty String on success, error message on failure
|
||||
Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
|
||||
String? result;
|
||||
try {
|
||||
result = await cancelPendingTransaction(txSlateId);
|
||||
Logging.instance.log("result?: $result", level: LogLevel.Info);
|
||||
final String wallet = (await _secureStore.read(
|
||||
key: '${_walletId}_wallet',
|
||||
))!;
|
||||
|
||||
final result = await m.protect(() async {
|
||||
return await compute(
|
||||
_cancelTransactionWrapper,
|
||||
Tuple2(
|
||||
wallet,
|
||||
txSlateId,
|
||||
),
|
||||
);
|
||||
});
|
||||
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();
|
||||
}
|
||||
return result!;
|
||||
}
|
||||
|
||||
//
|
||||
/// returns an empty String on success, error message on failure
|
||||
Future<String> cancelPendingTransaction(String txSlateId) async {
|
||||
final String wallet =
|
||||
(await _secureStore.read(key: '${_walletId}_wallet'))!;
|
||||
|
||||
String? result;
|
||||
await m.protect(() async {
|
||||
result = await compute(
|
||||
_cancelTransactionWrapper,
|
||||
Tuple2(
|
||||
wallet,
|
||||
txSlateId,
|
||||
),
|
||||
);
|
||||
});
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -492,7 +489,7 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
Logging.instance
|
||||
.log("this is a string $message", level: LogLevel.Error);
|
||||
stop(receivePort);
|
||||
throw Exception("txHttpSend isolate failed");
|
||||
throw Exception(message);
|
||||
}
|
||||
stop(receivePort);
|
||||
Logging.instance
|
||||
|
@ -667,25 +664,8 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
|
||||
await epicUpdateLastScannedBlock(await getRestoreHeight());
|
||||
|
||||
if (!await startScans()) {
|
||||
refreshMutex = false;
|
||||
GlobalEventBus.instance.fire(
|
||||
NodeConnectionStatusChangedEvent(
|
||||
NodeConnectionStatus.disconnected,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.unableToSync,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await refresh();
|
||||
await _startScans();
|
||||
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.synced,
|
||||
|
@ -694,12 +674,23 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
),
|
||||
);
|
||||
} 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;
|
||||
Logging.instance
|
||||
.log("$e, $s", level: LogLevel.Error, printFullLength: true);
|
||||
}
|
||||
refreshMutex = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -766,7 +757,9 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
await _prefs.init();
|
||||
await updateNode(false);
|
||||
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
|
||||
}
|
||||
|
||||
Future<bool> startScans() async {
|
||||
Future<void> _startScans() async {
|
||||
try {
|
||||
//First stop the current listener
|
||||
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!);
|
||||
}
|
||||
|
||||
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
||||
|
||||
var restoreHeight = epicGetRestoreHeight();
|
||||
var chainHeight = await this.chainHeight;
|
||||
if (epicGetLastScannedBlock() == null) {
|
||||
await epicUpdateLastScannedBlock(await getRestoreHeight());
|
||||
}
|
||||
int lastScannedBlock = epicGetLastScannedBlock()!;
|
||||
const MAX_PER_LOOP = 10000;
|
||||
await getSyncPercent;
|
||||
for (; lastScannedBlock < chainHeight;) {
|
||||
chainHeight = await this.chainHeight;
|
||||
lastScannedBlock = epicGetLastScannedBlock()!;
|
||||
Logging.instance.log(
|
||||
"chainHeight: $chainHeight, restoreHeight: $restoreHeight, lastScannedBlock: $lastScannedBlock",
|
||||
level: LogLevel.Info);
|
||||
int? nextScannedBlock;
|
||||
await m.protect(() async {
|
||||
ReceivePort receivePort = await getIsolate({
|
||||
"function": "scanOutPuts",
|
||||
"wallet": wallet!,
|
||||
"startHeight": lastScannedBlock,
|
||||
"numberOfBlocks": MAX_PER_LOOP,
|
||||
}, name: walletName);
|
||||
// max number of blocks to scan per loop iteration
|
||||
const scanChunkSize = 10000;
|
||||
|
||||
var message = await receivePort.first;
|
||||
if (message is String) {
|
||||
Logging.instance
|
||||
.log("this is a string $message", level: LogLevel.Error);
|
||||
stop(receivePort);
|
||||
throw Exception("scanOutPuts isolate failed");
|
||||
// force firing of scan progress event
|
||||
await getSyncPercent;
|
||||
|
||||
// fetch current chain height and last scanned block (should be the
|
||||
// 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(
|
||||
"chainHeight: $chainHeight, lastScannedBlock: $lastScannedBlock",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
final int nextScannedBlock = await m.protect(() async {
|
||||
ReceivePort? receivePort;
|
||||
try {
|
||||
receivePort = await getIsolate({
|
||||
"function": "scanOutPuts",
|
||||
"wallet": wallet!,
|
||||
"startHeight": lastScannedBlock,
|
||||
"numberOfBlocks": scanChunkSize,
|
||||
}, name: walletName);
|
||||
|
||||
// get response
|
||||
final message = await receivePort.first;
|
||||
|
||||
// check for error message
|
||||
if (message is String) {
|
||||
throw Exception("scanOutPuts isolate failed: $message");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
nextScannedBlock = int.parse(message['outputs'] as String);
|
||||
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;
|
||||
|
||||
// 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();
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("$e, $s", level: LogLevel.Warning);
|
||||
return false;
|
||||
Logging.instance.log(
|
||||
"_startScans failed: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1494,24 +1526,7 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
final int curAdd = await setCurrentIndex();
|
||||
await _getReceivingAddressForIndex(curAdd);
|
||||
|
||||
if (!await startScans()) {
|
||||
refreshMutex = false;
|
||||
GlobalEventBus.instance.fire(
|
||||
NodeConnectionStatusChangedEvent(
|
||||
NodeConnectionStatus.disconnected,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
GlobalEventBus.instance.fire(
|
||||
WalletSyncStatusChangedEvent(
|
||||
WalletSyncStatus.unableToSync,
|
||||
walletId,
|
||||
coin,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await _startScans();
|
||||
|
||||
unawaited(startSync());
|
||||
|
||||
|
@ -1685,26 +1700,13 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
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();
|
||||
|
||||
for (var tx in jsonTransactions) {
|
||||
Logging.instance.log("tx: $tx", level: LogLevel.Info);
|
||||
// // TODO: does "confirmed" mean finalized? If so please remove this todo
|
||||
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;
|
||||
if (tx["tx_type"] == "TxReceived" ||
|
||||
tx["tx_type"] == "TxReceivedCancelled") {
|
||||
|
@ -1725,7 +1727,6 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
||||
tx['numberOfMessages'] = tx['messages']?['messages']?.length;
|
||||
tx['onChainNote'] = tx['messages']?['messages']?[0]?['message'];
|
||||
print("ON CHAIN MESSAGE IS ${tx['onChainNote']}");
|
||||
|
||||
int? height;
|
||||
|
||||
|
@ -1738,7 +1739,6 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
final isIncoming = (tx["tx_type"] == "TxReceived" ||
|
||||
tx["tx_type"] == "TxReceivedCancelled");
|
||||
|
||||
|
||||
final txn = isar_models.Transaction(
|
||||
walletId: walletId,
|
||||
txid: commitId ?? tx["id"].toString(),
|
||||
|
@ -1763,7 +1763,9 @@ class EpicCashWallet extends CoinServiceAPI
|
|||
otherData: tx['onChainNote'].toString(),
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
numberOfMessages: ((tx["numberOfMessages"] == null) ? 0 : tx["numberOfMessages"]) as int,
|
||||
numberOfMessages: ((tx["numberOfMessages"] == null)
|
||||
? 0
|
||||
: tx["numberOfMessages"]) as int,
|
||||
);
|
||||
|
||||
// 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));
|
||||
// cachedMap?.remove(tx["id"].toString());
|
||||
// cachedMap?.remove(commitId);
|
||||
// Logging.instance.log("cmap: $cachedMap", level: LogLevel.Info);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -310,7 +310,9 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance.log(
|
||||
"Generating new ${coin.prettyName} wallet.",
|
||||
level: LogLevel.Info,
|
||||
|
@ -324,7 +326,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
await _prefs.init();
|
||||
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"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
|
||||
// .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
// if (!integrationTestFlag) {
|
||||
|
@ -366,14 +370,23 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"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}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: passphrase,
|
||||
);
|
||||
|
||||
await _generateAndSaveAddress(mnemonic, "");
|
||||
await _generateAndSaveAddress(mnemonic, passphrase);
|
||||
|
||||
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
|
||||
}
|
||||
|
|
|
@ -1875,7 +1875,9 @@ class FiroWallet extends CoinServiceAPI
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1886,7 +1888,7 @@ class FiroWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
level: LogLevel.Fatal);
|
||||
|
@ -2124,7 +2126,9 @@ class FiroWallet extends CoinServiceAPI
|
|||
}
|
||||
|
||||
/// 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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -2158,12 +2162,20 @@ class FiroWallet extends CoinServiceAPI
|
|||
longMutex = false;
|
||||
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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
|
@ -3340,6 +3352,30 @@ class FiroWallet extends CoinServiceAPI
|
|||
|
||||
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;
|
||||
|
||||
for (final txHash in allTxHashes) {
|
||||
|
|
|
@ -1285,7 +1285,9 @@ class LitecoinWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1296,7 +1298,7 @@ class LitecoinWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1570,12 +1574,20 @@ class LitecoinWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
|
|
|
@ -181,7 +181,9 @@ class Manager with ChangeNotifier {
|
|||
Future<bool> 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> recoverFromMnemonic({
|
||||
required String mnemonic,
|
||||
|
|
|
@ -307,7 +307,9 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
await _prefs.init();
|
||||
|
||||
// this should never fail
|
||||
|
|
|
@ -1268,7 +1268,9 @@ class NamecoinWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1279,7 +1281,7 @@ class NamecoinWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1544,12 +1548,20 @@ class NamecoinWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:nanodart/nanodart.dart';
|
||||
import 'package:stackwallet/db/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/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';
|
||||
|
@ -28,6 +28,7 @@ import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
|||
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
||||
import 'package:stackwallet/services/nano_api.dart';
|
||||
import 'package:stackwallet/services/node_service.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/services/transaction_notification_tracker.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
|
@ -154,10 +155,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Balance get balance => _balance ??= getCachedBalance();
|
||||
Balance? _balance;
|
||||
|
||||
HTTP client = HTTP();
|
||||
|
||||
Future<String?> requestWork(String hash) async {
|
||||
return http
|
||||
return client
|
||||
.post(
|
||||
Uri.parse("https://rpc.nano.to"), // this should be a
|
||||
url: Uri.parse("https://rpc.nano.to"), // this should be a
|
||||
headers: {'Content-type': 'application/json'},
|
||||
body: json.encode(
|
||||
{
|
||||
|
@ -165,9 +168,11 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"hash": hash,
|
||||
},
|
||||
),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
)
|
||||
.then((http.Response response) {
|
||||
if (response.statusCode == 200) {
|
||||
.then((Response response) {
|
||||
if (response.code == 200) {
|
||||
final Map<String, dynamic> decoded =
|
||||
json.decode(response.body) as Map<String, dynamic>;
|
||||
if (decoded.containsKey("error")) {
|
||||
|
@ -175,7 +180,7 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
return decoded["work"] as String?;
|
||||
} 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 = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final balanceResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final balanceResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
|
||||
|
@ -212,10 +219,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"representative": "true",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final String frontier =
|
||||
|
@ -265,10 +274,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"subtype": "send",
|
||||
"block": sendBlock,
|
||||
});
|
||||
final processResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final processResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> decoded =
|
||||
|
@ -333,8 +344,13 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: headers, body: body);
|
||||
final response = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: body,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final data = jsonDecode(response.body);
|
||||
_balance = Balance(
|
||||
total: Amount(
|
||||
|
@ -372,10 +388,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"representative": "true",
|
||||
"account": publicAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
|
@ -390,10 +408,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"account": publicAddress,
|
||||
});
|
||||
|
||||
final balanceResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final balanceResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
|
@ -463,10 +483,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"subtype": "receive",
|
||||
"block": receiveBlock,
|
||||
});
|
||||
final processResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final processResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> decoded =
|
||||
|
@ -477,14 +499,18 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
Future<void> confirmAllReceivable() async {
|
||||
final receivableResponse = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": await currentReceivingAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
final receivableResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": await currentReceivingAddress,
|
||||
"count": "-1",
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final receivableData = await jsonDecode(receivableResponse.body);
|
||||
if (receivableData["blocks"] == "") {
|
||||
|
@ -506,13 +532,17 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
await confirmAllReceivable();
|
||||
final receivingAddress = (await _currentReceivingAddress)!;
|
||||
final String publicAddress = receivingAddress.value;
|
||||
final response = await http.post(Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": publicAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
final response = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "account_history",
|
||||
"account": publicAddress,
|
||||
"count": "-1",
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final data = await jsonDecode(response.body);
|
||||
final transactions =
|
||||
data["history"] is List ? data["history"] as List<dynamic> : [];
|
||||
|
@ -607,7 +637,9 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
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!");
|
||||
|
@ -828,17 +860,19 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Future<bool> testNetworkConnection() async {
|
||||
final uri = Uri.parse(getCurrentNode().host);
|
||||
|
||||
final response = await http.post(
|
||||
uri,
|
||||
final response = await client.post(
|
||||
url: uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode(
|
||||
{
|
||||
"action": "version",
|
||||
},
|
||||
),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
return response.statusCode == 200;
|
||||
return response.code == 200;
|
||||
}
|
||||
|
||||
Timer? _networkAliveTimer;
|
||||
|
@ -924,10 +958,12 @@ class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
final infoResponse = await http.post(
|
||||
Uri.parse(getCurrentNode().host),
|
||||
final infoResponse = await client.post(
|
||||
url: Uri.parse(getCurrentNode().host),
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
|
|
|
@ -1195,7 +1195,9 @@ class ParticlWallet extends CoinServiceAPI
|
|||
bool get isConnected => _isConnected;
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data,
|
||||
) async {
|
||||
Logging.instance
|
||||
.log("Generating new ${coin.prettyName} wallet.", level: LogLevel.Info);
|
||||
|
||||
|
@ -1206,7 +1208,7 @@ class ParticlWallet extends CoinServiceAPI
|
|||
|
||||
await _prefs.init();
|
||||
try {
|
||||
await _generateNewWallet();
|
||||
await _generateNewWallet(data);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from initializeNew(): $e\n$s",
|
||||
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
|
||||
.log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
|
||||
if (!integrationTestFlag) {
|
||||
|
@ -1459,12 +1463,20 @@ class ParticlWallet extends CoinServiceAPI
|
|||
throw Exception(
|
||||
"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(
|
||||
key: '${_walletId}_mnemonic',
|
||||
value: bip39.generateMnemonic(strength: 128));
|
||||
value: bip39.generateMnemonic(strength: strength));
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_mnemonicPassphrase',
|
||||
value: "",
|
||||
value: data?.mnemonicPassphrase ?? "",
|
||||
);
|
||||
|
||||
// Generate and add addresses to relevant arrays
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stackwallet/db/isar/main_db.dart';
|
||||
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/logger.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:tuple/tuple.dart';
|
||||
|
||||
|
@ -53,20 +54,25 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
initCache(walletId, coin);
|
||||
initWalletDB(mockableOverride: mockableOverride);
|
||||
|
||||
if (coin.name == "stellarTestnet") {
|
||||
stellarSdk = StellarSDK.TESTNET;
|
||||
if (coin.isTestNet) {
|
||||
stellarNetwork = Network.TESTNET;
|
||||
} else {
|
||||
stellarSdk = StellarSDK.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 SecureStorageInterface _secureStore;
|
||||
|
||||
// final StellarSDK stellarSdk = StellarSDK.PUBLIC;
|
||||
|
||||
@override
|
||||
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
|
||||
bool? _isFavorite;
|
||||
|
@ -175,6 +181,41 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
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
|
||||
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
||||
final secretSeed = await _secureStore.read(key: '${_walletId}_secretSeed');
|
||||
|
@ -182,34 +223,41 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
AccountResponse sender =
|
||||
await stellarSdk.accounts.account(senderKeyPair.accountId);
|
||||
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,
|
||||
// the transaction fee will be charged when the transaction fails.
|
||||
bool validAccount = await _accountExists(txData['address'] as String);
|
||||
Transaction transaction;
|
||||
TransactionBuilder transactionBuilder;
|
||||
|
||||
if (!validAccount) {
|
||||
//Fund the account, user must ensure account is correct
|
||||
CreateAccountOperationBuilder createAccBuilder =
|
||||
CreateAccountOperationBuilder(
|
||||
txData['address'] as String, amountToSend.decimal.toString());
|
||||
transaction = TransactionBuilder(sender)
|
||||
.addOperation(createAccBuilder.build())
|
||||
.build();
|
||||
transactionBuilder =
|
||||
TransactionBuilder(sender).addOperation(createAccBuilder.build());
|
||||
} else {
|
||||
transaction = TransactionBuilder(sender)
|
||||
.addOperation(PaymentOperationBuilder(txData['address'] as String,
|
||||
Asset.NATIVE, amountToSend.decimal.toString())
|
||||
.build())
|
||||
.build();
|
||||
transactionBuilder = TransactionBuilder(sender).addOperation(
|
||||
PaymentOperationBuilder(txData['address'] as String, Asset.NATIVE,
|
||||
amountToSend.decimal.toString())
|
||||
.build());
|
||||
}
|
||||
|
||||
if (memo != null) {
|
||||
transactionBuilder.addMemo(MemoText(memo));
|
||||
}
|
||||
|
||||
final transaction = transactionBuilder.build();
|
||||
|
||||
transaction.sign(senderKeyPair, stellarNetwork);
|
||||
try {
|
||||
SubmitTransactionResponse response =
|
||||
await stellarSdk.submitTransaction(transaction);
|
||||
|
||||
SubmitTransactionResponse response = await stellarSdk
|
||||
.submitTransaction(transaction)
|
||||
.onError((error, stackTrace) => throw (error.toString()));
|
||||
if (!response.success) {
|
||||
throw ("Unable to send transaction");
|
||||
throw ("${response.extras?.resultCodes?.transactionResultCode}"
|
||||
" ::: ${response.extras?.resultCodes?.operationsResultCodes}");
|
||||
}
|
||||
return response.hash!;
|
||||
} catch (e, s) {
|
||||
|
@ -232,32 +280,15 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
(await _currentReceivingAddress)?.value ?? await getAddressSW();
|
||||
|
||||
Future<int> getBaseFee() async {
|
||||
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
final nodeURI = Uri.parse(getCurrentNode().host);
|
||||
final httpClient = http.Client();
|
||||
FeeStatsResponse fsp =
|
||||
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
|
||||
return int.parse(fsp.lastLedgerBaseFee);
|
||||
var fees = await stellarSdk.feeStats.execute();
|
||||
return int.parse(fees.lastLedgerBaseFee);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
||||
var baseFee = await getBaseFee();
|
||||
int fee = 100;
|
||||
switch (feeRate) {
|
||||
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);
|
||||
return Amount(
|
||||
rawValue: BigInt.from(baseFee), fractionDigits: coin.decimals);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -285,34 +316,74 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
@override
|
||||
Future<FeeObject> get fees async {
|
||||
// final nodeURI = Uri.parse("${getCurrentNode().host}:${getCurrentNode().port}");
|
||||
final nodeURI = Uri.parse(getCurrentNode().host);
|
||||
|
||||
final httpClient = http.Client();
|
||||
FeeStatsResponse fsp =
|
||||
await FeeStatsRequestBuilder(httpClient, nodeURI).execute();
|
||||
|
||||
int fee = await getBaseFee();
|
||||
return FeeObject(
|
||||
numberOfBlocksFast: 0,
|
||||
numberOfBlocksAverage: 0,
|
||||
numberOfBlocksSlow: 0,
|
||||
fast: int.parse(fsp.lastLedgerBaseFee) * 100,
|
||||
medium: int.parse(fsp.lastLedgerBaseFee) * 50,
|
||||
slow: int.parse(fsp.lastLedgerBaseFee) * 10);
|
||||
numberOfBlocksFast: 10,
|
||||
numberOfBlocksAverage: 10,
|
||||
numberOfBlocksSlow: 10,
|
||||
fast: fee,
|
||||
medium: fee,
|
||||
slow: fee);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> fullRescan(
|
||||
int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) async {
|
||||
await _prefs.init();
|
||||
await updateTransactions();
|
||||
await updateChainHeight();
|
||||
await updateBalance();
|
||||
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 _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
|
||||
Future<bool> generateNewAddress() {
|
||||
// TODO: implement generateNewAddress
|
||||
// not used for stellar(?)
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
@ -326,7 +397,9 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew() async {
|
||||
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!");
|
||||
|
@ -334,11 +407,26 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
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}_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);
|
||||
String address = keyPair.accountId;
|
||||
String secretSeed =
|
||||
|
@ -394,78 +482,87 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Future<String?> get mnemonicString =>
|
||||
_secureStore.read(key: '${_walletId}_mnemonic');
|
||||
|
||||
@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,
|
||||
};
|
||||
Future<void> _recoverWalletFromBIP32SeedPhrase({
|
||||
required String mnemonic,
|
||||
required String mnemonicPassphrase,
|
||||
bool isRescan = false,
|
||||
}) async {
|
||||
final Wallet wallet = await Wallet.from(
|
||||
mnemonic,
|
||||
passphrase: mnemonicPassphrase,
|
||||
);
|
||||
final KeyPair keyPair = await wallet.getKeyPair(index: 0);
|
||||
final String address = keyPair.accountId;
|
||||
String secretSeed =
|
||||
keyPair.secretSeed; //This will be required for sending a tx
|
||||
|
||||
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;
|
||||
await _secureStore.write(
|
||||
key: '${_walletId}_secretSeed',
|
||||
value: secretSeed,
|
||||
);
|
||||
|
||||
final swAddress = SWAddress.Address(
|
||||
walletId: walletId,
|
||||
value: address,
|
||||
publicKey: keyPair.publicKey,
|
||||
derivationIndex: 0,
|
||||
derivationPath: null,
|
||||
type: SWAddress.AddressType.unknown,
|
||||
subType: SWAddress.AddressSubType.unknown,
|
||||
);
|
||||
|
||||
if (isRescan) {
|
||||
await db.updateOrPutAddresses([swAddress]);
|
||||
} else {
|
||||
await db.putAddress(swAddress);
|
||||
}
|
||||
}
|
||||
|
||||
bool longMutex = false;
|
||||
|
||||
@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!");
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
walletId: walletId,
|
||||
value: address,
|
||||
publicKey: keyPair.publicKey,
|
||||
derivationIndex: 0,
|
||||
derivationPath: null,
|
||||
type: SWAddress.AddressType.unknown, // TODO: set type
|
||||
subType: SWAddress.AddressSubType.unknown);
|
||||
|
||||
await db.putAddress(swAddress);
|
||||
|
||||
await Future.wait(
|
||||
[updateCachedId(walletId), updateCachedIsFavorite(false)]);
|
||||
}
|
||||
|
||||
Future<void> updateChainHeight() async {
|
||||
|
@ -482,26 +579,34 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
try {
|
||||
List<Tuple2<SWTransaction.Transaction, SWAddress.Address?>>
|
||||
transactionList = [];
|
||||
|
||||
Page<OperationResponse> payments = await stellarSdk.payments
|
||||
.forAccount(await getAddressSW())
|
||||
.order(RequestBuilderOrder.DESC)
|
||||
.execute()
|
||||
.onError(
|
||||
(error, stackTrace) => throw ("Could not fetch transactions"));
|
||||
|
||||
Page<OperationResponse> payments;
|
||||
try {
|
||||
payments = await stellarSdk.payments
|
||||
.forAccount(await getAddressSW())
|
||||
.order(RequestBuilderOrder.DESC)
|
||||
.execute()
|
||||
.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 (OperationResponse response in payments.records!) {
|
||||
// PaymentOperationResponse por;
|
||||
if (response is PaymentOperationResponse) {
|
||||
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;
|
||||
if (por.sourceAccount == await getAddressSW()) {
|
||||
type = SWTransaction.TransactionType.outgoing;
|
||||
|
@ -628,13 +733,35 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
Logging.instance.log(
|
||||
"Exception rethrown from updateTransactions(): $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateBalance() async {
|
||||
try {
|
||||
AccountResponse accountResponse =
|
||||
await stellarSdk.accounts.account(await getAddressSW());
|
||||
AccountResponse accountResponse;
|
||||
|
||||
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) {
|
||||
switch (balance.assetType) {
|
||||
case Asset.TYPE_NATIVE:
|
||||
|
@ -665,6 +792,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
"ERROR GETTING BALANCE $e\n$s",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -739,9 +867,8 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
int get storedChainHeight => getCachedChainHeight();
|
||||
|
||||
@override
|
||||
Future<bool> testNetworkConnection() {
|
||||
// TODO: implement testNetworkConnection
|
||||
throw UnimplementedError();
|
||||
Future<bool> testNetworkConnection() async {
|
||||
return await testStellarNodeConnection(_xlmNode!.host, _xlmNode!.port);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -750,10 +877,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
|
||||
@override
|
||||
Future<void> updateNode(bool shouldRefresh) async {
|
||||
_xlmNode = NodeService(secureStorageInterface: _secureStore)
|
||||
.getPrimaryNodeFor(coin: coin) ??
|
||||
DefaultNodes.getNodeFor(coin);
|
||||
|
||||
_updateNode();
|
||||
if (shouldRefresh) {
|
||||
unawaited(refresh());
|
||||
}
|
||||
|
@ -795,7 +919,7 @@ class StellarWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
// TODO: implement utxos
|
||||
// not used
|
||||
Future<List<UTXO>> get utxos => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
|
|
776
lib/services/coins/tezos/tezos_wallet.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -333,7 +333,10 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> initializeNew({int seedWordsLength = 14}) async {
|
||||
Future<void> initializeNew(
|
||||
({String mnemonicPassphrase, int wordCount})? data, {
|
||||
int seedWordsLength = 14,
|
||||
}) async {
|
||||
await _prefs.init();
|
||||
|
||||
// this should never fail
|
||||
|
|
|
@ -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/models/isar/models/ethereum/eth_contract.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/default_nodes.dart';
|
||||
import 'package:stackwallet/utilities/eth_commons.dart';
|
||||
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class EthApiException implements Exception {
|
||||
|
@ -46,19 +49,23 @@ class EthereumResponse<T> {
|
|||
abstract class EthereumAPI {
|
||||
static String get stackBaseServer => DefaultNodes.ethereum.host;
|
||||
|
||||
static HTTP client = HTTP();
|
||||
|
||||
static Future<EthereumResponse<List<EthTxDTO>>> getEthTransactions({
|
||||
required String address,
|
||||
int firstBlock = 0,
|
||||
bool includeTokens = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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) {
|
||||
final json = jsonDecode(response.body) as Map;
|
||||
final list = json["data"] as List?;
|
||||
|
@ -86,7 +93,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getEthTransactions($address) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -173,13 +180,15 @@ abstract class EthereumAPI {
|
|||
List<EthTxDTO> txns,
|
||||
) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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) {
|
||||
final json = jsonDecode(response.body) as Map;
|
||||
final list = List<Map<String, dynamic>>.from(json["data"] as List);
|
||||
|
@ -208,7 +217,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getEthTransactionNonces($txns) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -231,13 +240,15 @@ abstract class EthereumAPI {
|
|||
static Future<EthereumResponse<List<EthTokenTxExtraDTO>>>
|
||||
getEthTokenTransactionsByTxids(List<String> txids) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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) {
|
||||
final json = jsonDecode(response.body) as Map;
|
||||
final list = json["data"] as List?;
|
||||
|
@ -257,13 +268,13 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getEthTokenTransactionsByTxids($txids) response is empty but status code is "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw EthApiException(
|
||||
"getEthTokenTransactionsByTxids($txids) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -288,13 +299,15 @@ abstract class EthereumAPI {
|
|||
required String tokenContractAddress,
|
||||
}) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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) {
|
||||
final json = jsonDecode(response.body) as Map;
|
||||
final list = json["data"] as List?;
|
||||
|
@ -321,7 +334,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getTokenTransactions($address, $tokenContractAddress) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -422,9 +435,13 @@ abstract class EthereumAPI {
|
|||
final uri = Uri.parse(
|
||||
"$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);
|
||||
if (json["data"] is List) {
|
||||
final map = json["data"].first as Map;
|
||||
|
@ -442,7 +459,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getWalletTokenBalance($address) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -469,9 +486,13 @@ abstract class EthereumAPI {
|
|||
final uri = Uri.parse(
|
||||
"$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);
|
||||
if (json["data"] is List) {
|
||||
final map = json["data"].first as Map;
|
||||
|
@ -488,7 +509,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getAddressNonce($address) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -510,13 +531,15 @@ abstract class EthereumAPI {
|
|||
|
||||
static Future<EthereumResponse<GasTracker>> getGasOracle() async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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;
|
||||
if (json["success"] == true) {
|
||||
try {
|
||||
|
@ -541,7 +564,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getGasOracle() failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -579,13 +602,15 @@ abstract class EthereumAPI {
|
|||
static Future<EthereumResponse<EthContract>> getTokenContractInfoByAddress(
|
||||
String contractAddress) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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;
|
||||
if (json["data"] is List) {
|
||||
final map = Map<String, dynamic>.from(json["data"].first as Map);
|
||||
|
@ -621,7 +646,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getTokenByContractAddress($contractAddress) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -646,13 +671,15 @@ abstract class EthereumAPI {
|
|||
required String contractAddress,
|
||||
}) async {
|
||||
try {
|
||||
final response = await get(
|
||||
Uri.parse(
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$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;
|
||||
|
||||
return EthereumResponse(
|
||||
|
@ -662,7 +689,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getTokenAbi($name, $contractAddress) failed with status code: "
|
||||
"${response.statusCode}",
|
||||
"${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
@ -687,9 +714,13 @@ abstract class EthereumAPI {
|
|||
String contractAddress,
|
||||
) async {
|
||||
try {
|
||||
final response = await get(Uri.parse(
|
||||
"$stackBaseServer/state?addrs=$contractAddress&parts=proxy"));
|
||||
if (response.statusCode == 200) {
|
||||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
"$stackBaseServer/state?addrs=$contractAddress&parts=proxy"),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (response.code == 200) {
|
||||
final json = jsonDecode(response.body);
|
||||
final list = json["data"] as List;
|
||||
final map = Map<String, dynamic>.from(list.first as Map);
|
||||
|
@ -701,7 +732,7 @@ abstract class EthereumAPI {
|
|||
} else {
|
||||
throw EthApiException(
|
||||
"getProxyTokenImplementationAddress($contractAddress) failed with"
|
||||
" status code: ${response.statusCode}",
|
||||
" status code: ${response.code}",
|
||||
);
|
||||
}
|
||||
} on EthApiException catch (e) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ import 'dart:convert';
|
|||
|
||||
import 'package:decimal/decimal.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/pair_unavailable_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/isar/exchange_cache/currency.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/exchange_response.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ChangeNowAPI {
|
||||
|
@ -37,12 +39,13 @@ class ChangeNowAPI {
|
|||
static const String apiVersion = "/v1";
|
||||
static const String apiVersionV2 = "/v2";
|
||||
|
||||
ChangeNowAPI._();
|
||||
static final ChangeNowAPI _instance = ChangeNowAPI._();
|
||||
static ChangeNowAPI get instance => _instance;
|
||||
final HTTP client;
|
||||
|
||||
/// set this to override using standard http client. Useful for testing
|
||||
http.Client? client;
|
||||
@visibleForTesting
|
||||
ChangeNowAPI({HTTP? http}) : client = http ?? HTTP();
|
||||
|
||||
static final ChangeNowAPI _instance = ChangeNowAPI();
|
||||
static ChangeNowAPI get instance => _instance;
|
||||
|
||||
Uri _buildUri(String path, Map<String, dynamic>? params) {
|
||||
return Uri.https(authority, apiVersion + path, params);
|
||||
|
@ -53,21 +56,23 @@ class ChangeNowAPI {
|
|||
}
|
||||
|
||||
Future<dynamic> _makeGetRequest(Uri uri) async {
|
||||
final client = this.client ?? http.Client();
|
||||
try {
|
||||
final response = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
String? data;
|
||||
try {
|
||||
final parsed = jsonDecode(response.body);
|
||||
data = response.body;
|
||||
final parsed = jsonDecode(data);
|
||||
|
||||
return parsed;
|
||||
} on FormatException catch (e) {
|
||||
return {
|
||||
"error": "Dart format exception",
|
||||
"message": response.body,
|
||||
"message": data,
|
||||
};
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
@ -78,17 +83,19 @@ class ChangeNowAPI {
|
|||
}
|
||||
|
||||
Future<dynamic> _makeGetRequestV2(Uri uri, String apiKey) async {
|
||||
final client = this.client ?? http.Client();
|
||||
try {
|
||||
final response = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
'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;
|
||||
} catch (e, s) {
|
||||
|
@ -102,21 +109,24 @@ class ChangeNowAPI {
|
|||
Uri uri,
|
||||
Map<String, String> body,
|
||||
) async {
|
||||
final client = this.client ?? http.Client();
|
||||
try {
|
||||
final response = await client.post(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(body),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
String? data;
|
||||
try {
|
||||
final parsed = jsonDecode(response.body);
|
||||
data = response.body;
|
||||
final parsed = jsonDecode(data);
|
||||
|
||||
return parsed;
|
||||
} catch (_) {
|
||||
Logging.instance.log("ChangeNOW api failed to parse: ${response.body}",
|
||||
level: LogLevel.Error);
|
||||
Logging.instance
|
||||
.log("ChangeNOW api failed to parse: $data", level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -274,4 +274,15 @@ class ChangeNowExchange extends Exchange {
|
|||
// TODO: implement getTrades
|
||||
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;
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -90,4 +90,25 @@ abstract class Exchange {
|
|||
Estimate? estimate,
|
||||
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.
|
||||
}
|
||||
|
|
|
@ -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/utilities/enums/exchange_rate_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/stack_file_system.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -144,6 +145,8 @@ class ExchangeDataLoadingService {
|
|||
);
|
||||
final start = DateTime.now();
|
||||
try {
|
||||
/*
|
||||
// Old exchange data loading code.
|
||||
await Future.wait([
|
||||
_loadChangeNowCurrencies(),
|
||||
// _loadChangeNowFixedRatePairs(),
|
||||
|
@ -157,6 +160,20 @@ class ExchangeDataLoadingService {
|
|||
// quicker to load available currencies on the fly for a specific base currency
|
||||
// await _loadChangeNowFixedRatePairs();
|
||||
// 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(
|
||||
"ExchangeDataLoadingService.loadAll finished in ${DateTime.now().difference(start).inSeconds} seconds",
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
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/majestic_bank/mb_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_status.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/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
||||
class MajesticBankAPI {
|
||||
static const String scheme = "https";
|
||||
|
@ -35,22 +37,23 @@ class MajesticBankAPI {
|
|||
|
||||
static MajesticBankAPI get instance => _instance;
|
||||
|
||||
/// set this to override using standard http client. Useful for testing
|
||||
http.Client? client;
|
||||
HTTP client = HTTP();
|
||||
|
||||
Uri _buildUri({required String endpoint, Map<String, String>? params}) {
|
||||
return Uri.https(authority, "/api/$version/$endpoint", params);
|
||||
}
|
||||
|
||||
Future<dynamic> _makeGetRequest(Uri uri) async {
|
||||
final client = this.client ?? http.Client();
|
||||
// final client = this.client ?? http.Client();
|
||||
int code = -1;
|
||||
try {
|
||||
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);
|
||||
|
||||
|
|
|
@ -324,4 +324,8 @@ class MajesticBankExchange extends Exchange {
|
|||
return ExchangeResponse(exception: response.exception);
|
||||
}
|
||||
}
|
||||
|
||||
// Majestic Bank supports tor.
|
||||
@override
|
||||
bool get supportsTor => true;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import 'dart:convert';
|
|||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
|
||||
import 'package:stackwallet/external_api_keys.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/simpleswap/sp_currency.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/simpleswap/simpleswap_exchange.dart';
|
||||
import 'package:stackwallet/services/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
|
@ -34,22 +36,22 @@ class SimpleSwapAPI {
|
|||
static final SimpleSwapAPI _instance = SimpleSwapAPI._();
|
||||
static SimpleSwapAPI get instance => _instance;
|
||||
|
||||
/// set this to override using standard http client. Useful for testing
|
||||
http.Client? client;
|
||||
HTTP client = HTTP();
|
||||
|
||||
Uri _buildUri(String path, Map<String, String>? params) {
|
||||
return Uri.https(authority, path, params);
|
||||
}
|
||||
|
||||
Future<dynamic> _makeGetRequest(Uri uri) async {
|
||||
final client = this.client ?? http.Client();
|
||||
int code = -1;
|
||||
try {
|
||||
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);
|
||||
|
||||
|
@ -67,15 +69,16 @@ class SimpleSwapAPI {
|
|||
Uri uri,
|
||||
Map<String, dynamic> body,
|
||||
) async {
|
||||
final client = this.client ?? http.Client();
|
||||
try {
|
||||
final response = await client.post(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(body),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (response.code == 200) {
|
||||
final parsed = jsonDecode(response.body);
|
||||
return parsed;
|
||||
}
|
||||
|
|
|
@ -12,14 +12,16 @@ import 'dart:convert';
|
|||
|
||||
import 'package:flutter/foundation.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/networking/http.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_rate.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/tor_service.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
||||
const kTrocadorApiKey = "8rFqf7QLxX1mUBiNPEMaLUpV2biz6n";
|
||||
const kTrocadorRefCode = "9eHm9BkQfS";
|
||||
|
@ -31,6 +33,7 @@ abstract class TrocadorAPI {
|
|||
|
||||
static const String markup = "1";
|
||||
static const String minKYCRating = "C";
|
||||
static HTTP client = HTTP();
|
||||
|
||||
static Uri _buildUri({
|
||||
required String method,
|
||||
|
@ -46,12 +49,14 @@ abstract class TrocadorAPI {
|
|||
int code = -1;
|
||||
try {
|
||||
debugPrint("URI: $uri");
|
||||
final response = await http.get(
|
||||
uri,
|
||||
final response = await client.get(
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
code = response.statusCode;
|
||||
code = response.code;
|
||||
|
||||
debugPrint("CODE: $code");
|
||||
debugPrint("BODY: ${response.body}");
|
||||
|
|
|
@ -401,4 +401,8 @@ class TrocadorExchange extends Exchange {
|
|||
return ExchangeResponse(exception: response.exception);
|
||||
}
|
||||
}
|
||||
|
||||
// Trocador supports Tor.
|
||||
@override
|
||||
bool get supportsTor => true;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:stackwallet/dto/ordinals/inscription_data.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 {
|
||||
static final LitescribeAPI _instance = LitescribeAPI._internal();
|
||||
|
@ -13,12 +15,17 @@ class LitescribeAPI {
|
|||
}
|
||||
|
||||
LitescribeAPI._internal();
|
||||
HTTP client = HTTP();
|
||||
|
||||
late String baseUrl;
|
||||
|
||||
Future<LitescribeResponse> _getResponse(String endpoint) async {
|
||||
final response = await http.get(Uri.parse('$baseUrl$endpoint'));
|
||||
if (response.statusCode == 200) {
|
||||
final response = await client.get(
|
||||
url: Uri.parse('$baseUrl$endpoint'),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
if (response.code == 200) {
|
||||
return LitescribeResponse(data: _validateJson(response.body));
|
||||
} else {
|
||||
throw Exception(
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
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/prefs.dart';
|
||||
|
||||
final pMonKeyService = Provider((ref) => MonKeyService());
|
||||
|
||||
class MonKeyService {
|
||||
static const baseURL = "https://monkey.banano.cc/api/v1/monkey/";
|
||||
HTTP client = HTTP();
|
||||
|
||||
Future<Uint8List> fetchMonKey({
|
||||
required String address,
|
||||
|
@ -20,13 +23,17 @@ class MonKeyService {
|
|||
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) {
|
||||
return response.bodyBytes;
|
||||
if (response.code == 200) {
|
||||
return Uint8List.fromList(response.bodyBytes);
|
||||
} else {
|
||||
throw Exception(
|
||||
"statusCode=${response.statusCode} body=${response.body}",
|
||||
"statusCode=${response.code} body=${response.body}",
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
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 {
|
||||
static Future<
|
||||
|
@ -16,9 +18,11 @@ class NanoAPI {
|
|||
NAccountInfo? accountInfo;
|
||||
Exception? exception;
|
||||
|
||||
HTTP client = HTTP();
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
server,
|
||||
final response = await client.post(
|
||||
url: server,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
@ -27,6 +31,8 @@ class NanoAPI {
|
|||
"representative": "true",
|
||||
"account": account,
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final map = jsonDecode(response.body);
|
||||
|
@ -105,8 +111,10 @@ class NanoAPI {
|
|||
required Uri server,
|
||||
required Map<String, dynamic> block,
|
||||
}) async {
|
||||
final response = await http.post(
|
||||
server,
|
||||
HTTP client = HTTP();
|
||||
|
||||
final response = await client.post(
|
||||
url: server,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
@ -116,6 +124,8 @@ class NanoAPI {
|
|||
"subtype": "change",
|
||||
"block": block,
|
||||
}),
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
return jsonDecode(response.body);
|
||||
|
|
|
@ -13,8 +13,9 @@ import 'dart:convert';
|
|||
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.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/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
|
@ -32,7 +33,7 @@ class PriceAPI {
|
|||
static const Duration refreshIntervalDuration =
|
||||
Duration(seconds: refreshInterval);
|
||||
|
||||
final Client client;
|
||||
final HTTP client;
|
||||
|
||||
PriceAPI(this.client);
|
||||
|
||||
|
@ -96,16 +97,18 @@ class PriceAPI {
|
|||
}
|
||||
Map<Coin, Tuple2<Decimal, double>> result = {};
|
||||
try {
|
||||
final uri =
|
||||
Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency"
|
||||
"=${baseCurrency.toLowerCase()}"
|
||||
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
|
||||
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar"
|
||||
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
|
||||
final uri = Uri.parse(
|
||||
"https://api.coingecko.com/api/v3/coins/markets?vs_currency"
|
||||
"=${baseCurrency.toLowerCase()}"
|
||||
"&ids=monero,bitcoin,litecoin,ecash,epic-cash,zcoin,dogecoin,"
|
||||
"bitcoin-cash,namecoin,wownero,ethereum,particl,nano,banano,stellar,tezos"
|
||||
"&order=market_cap_desc&per_page=50&page=1&sparkline=false");
|
||||
|
||||
final coinGeckoResponse = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List<dynamic>;
|
||||
|
@ -136,6 +139,8 @@ class PriceAPI {
|
|||
|
||||
static Future<List<String>?> availableBaseCurrencies() async {
|
||||
final externalCalls = Prefs.instance.externalCalls;
|
||||
HTTP client = HTTP();
|
||||
|
||||
if ((!Logger.isTestEnv && !externalCalls) ||
|
||||
!(await Prefs.instance.isExternalCallsSet())) {
|
||||
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";
|
||||
try {
|
||||
final uri = Uri.parse(uriString);
|
||||
final response = await Client().get(
|
||||
uri,
|
||||
final response = await client.get(
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final json = jsonDecode(response.body) as List<dynamic>;
|
||||
|
@ -186,8 +193,10 @@ class PriceAPI {
|
|||
"=$contractAddressesString&include_24hr_change=true");
|
||||
|
||||
final coinGeckoResponse = await client.get(
|
||||
uri,
|
||||
url: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
proxyInfo:
|
||||
Prefs.instance.useTor ? TorService.sharedInstance.proxyInfo : null,
|
||||
);
|
||||
|
||||
final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map;
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'dart:async';
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:stackwallet/networking/http.dart';
|
||||
import 'package:stackwallet/services/price.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -29,7 +30,7 @@ class PriceService extends ChangeNotifier {
|
|||
|
||||
final Map<String, Tuple2<Decimal, double>> _cachedTokenPrices = {};
|
||||
|
||||
final _priceAPI = PriceAPI(Client());
|
||||
final _priceAPI = PriceAPI(HTTP());
|
||||
|
||||
Tuple2<Decimal, double> getPrice(Coin coin) => _cachedPrices[coin]!;
|
||||
|
||||
|
|
142
lib/services/tor_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ class CoinThemeColorDefault {
|
|||
Color get stellar => const Color(0xFF6600FF);
|
||||
Color get nano => const Color(0xFF209CE9);
|
||||
Color get banano => const Color(0xFFFBDD11);
|
||||
Color get tezos => const Color(0xFF0F61FF);
|
||||
|
||||
Color forCoin(Coin coin) {
|
||||
switch (coin) {
|
||||
|
@ -70,6 +71,8 @@ class CoinThemeColorDefault {
|
|||
return nano;
|
||||
case Coin.banano:
|
||||
return banano;
|
||||
case Coin.tezos:
|
||||
return tezos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|