mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-19 00:54:33 +00:00
Merge remote-tracking branch 'origin/staging' into apple-docs
This commit is contained in:
commit
5c6f507f93
147 changed files with 18199 additions and 2545 deletions
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
|||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.10.6'
|
||||
flutter-version: '3.16.0'
|
||||
channel: 'stable'
|
||||
- name: Setup | Rust
|
||||
uses: ATiltedTree/setup-rust@v1
|
||||
|
|
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -7,3 +7,6 @@
|
|||
[submodule "crypto_plugins/flutter_liblelantus"]
|
||||
path = crypto_plugins/flutter_liblelantus
|
||||
url = https://github.com/cypherstack/flutter_liblelantus.git
|
||||
[submodule "crypto_plugins/frostdart"]
|
||||
path = crypto_plugins/frostdart
|
||||
url = https://github.com/cypherstack/frostdart
|
||||
|
|
BIN
assets/images/mascot.png
Normal file
BIN
assets/images/mascot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 KiB |
|
@ -1 +1 @@
|
|||
Subproject commit 5566f2bdb3d960cbda44e049a2ec11c363053dab
|
||||
Subproject commit aab6a4676188901fbe158d8f1feeb1fc0ea247f8
|
1
crypto_plugins/frostdart
Submodule
1
crypto_plugins/frostdart
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 0fbc038a262e3c2d82c7c6e34e194e9a47011d91
|
|
@ -85,6 +85,7 @@ class DbVersionMigrator with WalletDB {
|
|||
useSSL: node.useSSL),
|
||||
prefs: prefs,
|
||||
failovers: failovers,
|
||||
coin: Coin.firo,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -21,6 +21,7 @@ import 'package:stackwallet/models/isar/stack_theme.dart';
|
|||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/stack_file_system.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/spark_coin.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/token_wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||
|
@ -67,6 +68,7 @@ class MainDB {
|
|||
SparkCoinSchema,
|
||||
WalletInfoMetaSchema,
|
||||
TokenWalletInfoSchema,
|
||||
FrostWalletInfoSchema,
|
||||
],
|
||||
directory: (await StackFileSystem.applicationIsarDirectory()).path,
|
||||
// inspector: kDebugMode,
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
|
||||
import 'package:electrum_adapter/electrum_adapter.dart';
|
||||
import 'package:electrum_adapter/methods/specific/firo.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
|
@ -19,20 +22,41 @@ import 'package:string_validator/string_validator.dart';
|
|||
|
||||
class CachedElectrumXClient {
|
||||
final ElectrumXClient electrumXClient;
|
||||
ElectrumClient electrumAdapterClient;
|
||||
final Future<ElectrumClient> Function() electrumAdapterUpdateCallback;
|
||||
|
||||
static const minCacheConfirms = 30;
|
||||
|
||||
const CachedElectrumXClient({
|
||||
CachedElectrumXClient({
|
||||
required this.electrumXClient,
|
||||
required this.electrumAdapterClient,
|
||||
required this.electrumAdapterUpdateCallback,
|
||||
});
|
||||
|
||||
factory CachedElectrumXClient.from({
|
||||
required ElectrumXClient electrumXClient,
|
||||
required ElectrumClient electrumAdapterClient,
|
||||
required Future<ElectrumClient> Function() electrumAdapterUpdateCallback,
|
||||
}) =>
|
||||
CachedElectrumXClient(
|
||||
electrumXClient: electrumXClient,
|
||||
electrumAdapterClient: electrumAdapterClient,
|
||||
electrumAdapterUpdateCallback: electrumAdapterUpdateCallback,
|
||||
);
|
||||
|
||||
/// If the client is closed, use the callback to update it.
|
||||
_checkElectrumAdapterClient() async {
|
||||
if (electrumAdapterClient.peer.isClosed) {
|
||||
Logging.instance.log(
|
||||
"ElectrumAdapterClient is closed, reopening it...",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
ElectrumClient? _electrumAdapterClient =
|
||||
await electrumAdapterUpdateCallback.call();
|
||||
electrumAdapterClient = _electrumAdapterClient;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getAnonymitySet({
|
||||
required String groupId,
|
||||
String blockhash = "",
|
||||
|
@ -56,9 +80,12 @@ class CachedElectrumXClient {
|
|||
set = Map<String, dynamic>.from(cachedSet);
|
||||
}
|
||||
|
||||
final newSet = await electrumXClient.getLelantusAnonymitySet(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final newSet = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getLelantusAnonymitySet(
|
||||
groupId: groupId,
|
||||
blockhash: set["blockHash"] as String,
|
||||
blockHash: set["blockHash"] as String,
|
||||
);
|
||||
|
||||
// update set with new data
|
||||
|
@ -82,7 +109,7 @@ class CachedElectrumXClient {
|
|||
translatedCoin.add(!isHexadecimal(newCoin[2] as String)
|
||||
? base64ToHex(newCoin[2] as String)
|
||||
: newCoin[2]);
|
||||
} catch (e, s) {
|
||||
} catch (e) {
|
||||
translatedCoin.add(newCoin[2]);
|
||||
}
|
||||
translatedCoin.add(!isHexadecimal(newCoin[3] as String)
|
||||
|
@ -130,7 +157,10 @@ class CachedElectrumXClient {
|
|||
set = Map<String, dynamic>.from(cachedSet);
|
||||
}
|
||||
|
||||
final newSet = await electrumXClient.getSparkAnonymitySet(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final newSet = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkAnonymitySet(
|
||||
coinGroupId: groupId,
|
||||
startBlockHash: set["blockHash"] as String,
|
||||
);
|
||||
|
@ -188,8 +218,10 @@ class CachedElectrumXClient {
|
|||
|
||||
final cachedTx = box.get(txHash) as Map?;
|
||||
if (cachedTx == null) {
|
||||
final Map<String, dynamic> result = await electrumXClient
|
||||
.getTransaction(txHash: txHash, verbose: verbose);
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final Map<String, dynamic> result =
|
||||
await electrumAdapterClient.getTransaction(txHash);
|
||||
|
||||
result.remove("hex");
|
||||
result.remove("lelantusData");
|
||||
|
@ -231,7 +263,10 @@ class CachedElectrumXClient {
|
|||
cachedSerials.length - 100, // 100 being some arbitrary buffer
|
||||
);
|
||||
|
||||
final serials = await electrumXClient.getLelantusUsedCoinSerials(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final serials = await (electrumAdapterClient as FiroElectrumClient)
|
||||
.getLelantusUsedCoinSerials(
|
||||
startNumber: startNumber,
|
||||
);
|
||||
|
||||
|
@ -279,7 +314,10 @@ class CachedElectrumXClient {
|
|||
cachedTags.length - 100, // 100 being some arbitrary buffer
|
||||
);
|
||||
|
||||
final tags = await electrumXClient.getSparkUsedCoinsTags(
|
||||
await _checkElectrumAdapterClient();
|
||||
|
||||
final tags =
|
||||
await (electrumAdapterClient as FiroElectrumClient).getUsedCoinsTags(
|
||||
startNumber: startNumber,
|
||||
);
|
||||
|
||||
|
@ -287,12 +325,18 @@ class CachedElectrumXClient {
|
|||
// .map((e) => !isHexadecimal(e) ? base64ToHex(e) : e)
|
||||
// .toSet();
|
||||
|
||||
// Convert the Map<String, dynamic> tags to a Set<Object?>.
|
||||
final newTags = (tags["tags"] as List).toSet();
|
||||
|
||||
// ensure we are getting some overlap so we know we are not missing any
|
||||
if (cachedTags.isNotEmpty && tags.isNotEmpty) {
|
||||
assert(cachedTags.intersection(tags).isNotEmpty);
|
||||
assert(cachedTags.intersection(newTags).isNotEmpty);
|
||||
}
|
||||
|
||||
cachedTags.addAll(tags);
|
||||
// Make newTags an Iterable<String>.
|
||||
final Iterable<String> iterableTags = newTags.map((e) => e.toString());
|
||||
|
||||
cachedTags.addAll(iterableTags);
|
||||
|
||||
await box.put(
|
||||
"tags",
|
||||
|
|
149
lib/electrumx_rpc/electrumx_chain_height_service.dart
Normal file
149
lib/electrumx_rpc/electrumx_chain_height_service.dart
Normal file
|
@ -0,0 +1,149 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:electrum_adapter/electrum_adapter.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
|
||||
/// Manage chain height subscriptions for each coin.
|
||||
abstract class ChainHeightServiceManager {
|
||||
// A map of chain height services for each coin.
|
||||
static final Map<Coin, ChainHeightService> _services = {};
|
||||
// Map<Coin, ChainHeightService> get services => _services;
|
||||
|
||||
// Get the chain height service for a specific coin.
|
||||
static ChainHeightService? getService(Coin coin) {
|
||||
return _services[coin];
|
||||
}
|
||||
|
||||
// Add a chain height service for a specific coin.
|
||||
static void add(ChainHeightService service, Coin coin) {
|
||||
// Don't add a new service if one already exists.
|
||||
if (_services[coin] == null) {
|
||||
_services[coin] = service;
|
||||
} else {
|
||||
throw Exception("Chain height service for $coin already managed");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a chain height service for a specific coin.
|
||||
static void remove(Coin coin) {
|
||||
_services.remove(coin);
|
||||
}
|
||||
|
||||
// Close all subscriptions and clean up resources.
|
||||
static Future<void> dispose() async {
|
||||
// Close each subscription.
|
||||
//
|
||||
// Create a list of keys to avoid concurrent modification during iteration
|
||||
var keys = List<Coin>.from(_services.keys);
|
||||
|
||||
// Iterate over the copy of the keys
|
||||
for (final coin in keys) {
|
||||
final ChainHeightService? service = getService(coin);
|
||||
await service?.cancelListen();
|
||||
remove(coin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A service to fetch and listen for chain height updates.
|
||||
///
|
||||
/// TODO: Add error handling and branching to handle various other scenarios.
|
||||
class ChainHeightService {
|
||||
// The electrum_adapter client to use for fetching chain height updates.
|
||||
ElectrumClient client;
|
||||
|
||||
// The subscription to listen for chain height updates.
|
||||
StreamSubscription<dynamic>? _subscription;
|
||||
|
||||
// Whether the service has started listening for updates.
|
||||
bool get started => _subscription != null;
|
||||
|
||||
// The current chain height.
|
||||
int? _height;
|
||||
int? get height => _height;
|
||||
|
||||
// Whether the service is currently reconnecting.
|
||||
bool _isReconnecting = false;
|
||||
|
||||
// The reconnect timer.
|
||||
Timer? _reconnectTimer;
|
||||
|
||||
// The reconnection timeout duration.
|
||||
static const Duration _connectionTimeout = Duration(seconds: 10);
|
||||
|
||||
ChainHeightService({required this.client});
|
||||
|
||||
/// Fetch the current chain height and start listening for updates.
|
||||
Future<int> fetchHeightAndStartListenForUpdates() async {
|
||||
// Don't start a new subscription if one already exists.
|
||||
if (_subscription != null) {
|
||||
throw Exception(
|
||||
"Attempted to start a chain height service where an existing"
|
||||
" subscription already exists!",
|
||||
);
|
||||
}
|
||||
|
||||
// A completer to wait for the current chain height to be fetched.
|
||||
final completer = Completer<int>();
|
||||
|
||||
// Fetch the current chain height.
|
||||
_subscription = client.subscribeHeaders().listen((BlockHeader event) {
|
||||
_height = event.height;
|
||||
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(_height);
|
||||
}
|
||||
});
|
||||
|
||||
_subscription?.onError((dynamic error) {
|
||||
_handleError(error);
|
||||
});
|
||||
|
||||
// Wait for the current chain height to be fetched.
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Handle an error from the subscription.
|
||||
void _handleError(dynamic error) {
|
||||
Logging.instance.log(
|
||||
"Error reconnecting for chain height: ${error.toString()}",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
_attemptReconnect();
|
||||
}
|
||||
|
||||
/// Attempt to reconnect to the electrum server.
|
||||
void _attemptReconnect() {
|
||||
// Avoid multiple reconnection attempts.
|
||||
if (_isReconnecting) return;
|
||||
_isReconnecting = true;
|
||||
|
||||
// Attempt to reconnect.
|
||||
unawaited(fetchHeightAndStartListenForUpdates().then((_) {
|
||||
_isReconnecting = false;
|
||||
}));
|
||||
|
||||
// Set a timer to on the reconnection attempt and clean up if it fails.
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(_connectionTimeout, () async {
|
||||
if (_subscription == null) {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null; // Will also occur on an error via handleError.
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = null;
|
||||
_isReconnecting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop listening for chain height updates.
|
||||
Future<void> cancelListen() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
_reconnectTimer?.cancel();
|
||||
}
|
||||
}
|
|
@ -9,24 +9,28 @@
|
|||
*/
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:decimal/decimal.dart';
|
||||
import 'package:electrum_adapter/electrum_adapter.dart' as electrum_adapter;
|
||||
import 'package:electrum_adapter/electrum_adapter.dart';
|
||||
import 'package:electrum_adapter/methods/specific/firo.dart';
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx_chain_height_service.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/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
class WifiOnlyException implements Exception {}
|
||||
|
||||
|
@ -73,6 +77,12 @@ class ElectrumXClient {
|
|||
JsonRPC? get rpcClient => _rpcClient;
|
||||
JsonRPC? _rpcClient;
|
||||
|
||||
StreamChannel<dynamic>? get electrumAdapterChannel => _electrumAdapterChannel;
|
||||
StreamChannel<dynamic>? _electrumAdapterChannel;
|
||||
|
||||
ElectrumClient? get electrumAdapterClient => _electrumAdapterClient;
|
||||
ElectrumClient? _electrumAdapterClient;
|
||||
|
||||
late Prefs _prefs;
|
||||
late TorService _torService;
|
||||
|
||||
|
@ -81,6 +91,9 @@ class ElectrumXClient {
|
|||
|
||||
final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
|
||||
|
||||
Coin? get coin => _coin;
|
||||
late Coin? _coin;
|
||||
|
||||
// add finalizer to cancel stream subscription when all references to an
|
||||
// instance of ElectrumX becomes inaccessible
|
||||
static final Finalizer<ElectrumXClient> _finalizer = Finalizer(
|
||||
|
@ -101,7 +114,7 @@ class ElectrumXClient {
|
|||
required bool useSSL,
|
||||
required Prefs prefs,
|
||||
required List<ElectrumXNode> failovers,
|
||||
JsonRPC? client,
|
||||
Coin? coin,
|
||||
this.connectionTimeoutForSpecialCaseJsonRPCClients =
|
||||
const Duration(seconds: 60),
|
||||
TorService? torService,
|
||||
|
@ -112,9 +125,11 @@ class ElectrumXClient {
|
|||
_host = host;
|
||||
_port = port;
|
||||
_useSSL = useSSL;
|
||||
_rpcClient = client;
|
||||
_coin = coin;
|
||||
|
||||
final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
||||
|
||||
// Listen for tor status changes.
|
||||
_torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
|
||||
(event) async {
|
||||
switch (event.newStatus) {
|
||||
|
@ -133,6 +148,8 @@ class ElectrumXClient {
|
|||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for tor preference changes.
|
||||
_torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
||||
(event) async {
|
||||
// not sure if we need to do anything specific here
|
||||
|
@ -141,21 +158,13 @@ class ElectrumXClient {
|
|||
// 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;
|
||||
_electrumAdapterChannel = null;
|
||||
_electrumAdapterClient = null;
|
||||
|
||||
await temp?.disconnect(
|
||||
reason: "Tor status changed to \"${event.status}\"",
|
||||
);
|
||||
// Also close any chain height services that are currently open.
|
||||
await ChainHeightServiceManager.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -164,6 +173,7 @@ class ElectrumXClient {
|
|||
required ElectrumXNode node,
|
||||
required Prefs prefs,
|
||||
required List<ElectrumXNode> failovers,
|
||||
required Coin coin,
|
||||
TorService? torService,
|
||||
EventBus? globalEventBusForTesting,
|
||||
}) {
|
||||
|
@ -175,6 +185,7 @@ class ElectrumXClient {
|
|||
torService: torService,
|
||||
failovers: failovers,
|
||||
globalEventBusForTesting: globalEventBusForTesting,
|
||||
coin: coin,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -186,7 +197,9 @@ class ElectrumXClient {
|
|||
return true;
|
||||
}
|
||||
|
||||
void _checkRpcClient() {
|
||||
Future<void> checkElectrumAdapter() async {
|
||||
({InternetAddress host, int port})? proxyInfo;
|
||||
|
||||
// If we're supposed to use Tor...
|
||||
if (_prefs.useTor) {
|
||||
// But Tor isn't running...
|
||||
|
@ -195,64 +208,93 @@ class ElectrumXClient {
|
|||
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",
|
||||
"Tor preference set but Tor is not enabled, killswitch not set, connecting to Electrum adapter 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");
|
||||
"Tor preference and killswitch set but Tor is not enabled, not connecting to Electrum adapter");
|
||||
// TODO [prio=low]: Try to start Tor.
|
||||
}
|
||||
} else {
|
||||
// Get the proxy info from the TorService.
|
||||
final proxyInfo = _torService.getProxyInfo();
|
||||
|
||||
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;
|
||||
proxyInfo = _torService.getProxyInfo();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
// TODO [prio=med]: Add proxyInfo to StreamChannel (or add to wrapper).
|
||||
// if (_electrumAdapter!.proxyInfo != proxyInfo) {
|
||||
// _electrumAdapter!.proxyInfo = proxyInfo;
|
||||
// _electrumAdapter!.disconnect(
|
||||
// reason: "Tor proxyInfo does not match current info",
|
||||
// );
|
||||
// }
|
||||
|
||||
// If the current ElectrumAdapterClient is closed, create a new one.
|
||||
if (_electrumAdapterClient != null &&
|
||||
_electrumAdapterClient!.peer.isClosed) {
|
||||
_electrumAdapterChannel = null;
|
||||
_electrumAdapterClient = null;
|
||||
}
|
||||
|
||||
if (currentFailoverIndex == -1) {
|
||||
_electrumAdapterChannel ??= await electrum_adapter.connect(
|
||||
host,
|
||||
port: port,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
acceptUnverified: true,
|
||||
useSSL: useSSL,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
|
||||
_electrumAdapterClient ??= FiroElectrumClient(
|
||||
_electrumAdapterChannel!,
|
||||
host,
|
||||
port,
|
||||
useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
} else {
|
||||
_electrumAdapterClient ??= ElectrumClient(
|
||||
_electrumAdapterChannel!,
|
||||
host,
|
||||
port,
|
||||
useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_electrumAdapterChannel ??= await electrum_adapter.connect(
|
||||
failovers![currentFailoverIndex].address,
|
||||
port: failovers![currentFailoverIndex].port,
|
||||
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
aliveTimerDuration: connectionTimeoutForSpecialCaseJsonRPCClients,
|
||||
acceptUnverified: true,
|
||||
useSSL: failovers![currentFailoverIndex].useSSL,
|
||||
proxyInfo: proxyInfo,
|
||||
);
|
||||
if (_coin == Coin.firo || _coin == Coin.firoTestNet) {
|
||||
_electrumAdapterClient ??= FiroElectrumClient(
|
||||
_electrumAdapterChannel!,
|
||||
failovers![currentFailoverIndex].address,
|
||||
failovers![currentFailoverIndex].port,
|
||||
failovers![currentFailoverIndex].useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
} else {
|
||||
_electrumAdapterClient ??= ElectrumClient(
|
||||
_electrumAdapterChannel!,
|
||||
failovers![currentFailoverIndex].address,
|
||||
failovers![currentFailoverIndex].port,
|
||||
failovers![currentFailoverIndex].useSSL,
|
||||
proxyInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// Send raw rpc command
|
||||
|
@ -268,32 +310,22 @@ class ElectrumXClient {
|
|||
}
|
||||
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
await _torConnectingLock
|
||||
.protect(() async => await checkElectrumAdapter());
|
||||
} else {
|
||||
_checkRpcClient();
|
||||
await checkElectrumAdapter();
|
||||
}
|
||||
|
||||
try {
|
||||
final requestId = requestID ?? const Uuid().v1();
|
||||
final jsonArgs = json.encode(args);
|
||||
final jsonRequestString = '{"jsonrpc": "2.0", '
|
||||
'"id": "$requestId",'
|
||||
'"method": "$command",'
|
||||
'"params": $jsonArgs}';
|
||||
|
||||
// Logging.instance.log("ElectrumX jsonRequestString: $jsonRequestString");
|
||||
|
||||
final response = await _rpcClient!.request(
|
||||
jsonRequestString,
|
||||
requestTimeout,
|
||||
final response = await _electrumAdapterClient!.request(
|
||||
command,
|
||||
args,
|
||||
);
|
||||
|
||||
if (response.exception != null) {
|
||||
throw response.exception!;
|
||||
}
|
||||
|
||||
if (response.data is Map && response.data["error"] != null) {
|
||||
if (response.data["error"]
|
||||
if (response is Map &&
|
||||
response.keys.contains("error") &&
|
||||
response["error"] != null) {
|
||||
if (response["error"]
|
||||
.toString()
|
||||
.contains("No such mempool or blockchain transaction")) {
|
||||
throw NoSuchTransactionException(
|
||||
|
@ -305,13 +337,19 @@ class ElectrumXClient {
|
|||
throw Exception(
|
||||
"JSONRPC response\n"
|
||||
" command: $command\n"
|
||||
" error: ${response.data}"
|
||||
" error: ${response["error"]}\n"
|
||||
" args: $args\n",
|
||||
);
|
||||
}
|
||||
|
||||
currentFailoverIndex = -1;
|
||||
return response.data;
|
||||
|
||||
// If the command is a ping, a good return should always be null.
|
||||
if (command.contains("ping")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return response;
|
||||
} on WifiOnlyException {
|
||||
rethrow;
|
||||
} on SocketException {
|
||||
|
@ -347,9 +385,9 @@ class ElectrumXClient {
|
|||
/// map of <request id string : arguments list>
|
||||
///
|
||||
/// returns a list of json response objects if no errors were found
|
||||
Future<List<Map<String, dynamic>>> batchRequest({
|
||||
Future<List<dynamic>> batchRequest({
|
||||
required String command,
|
||||
required Map<String, List<dynamic>> args,
|
||||
required List<dynamic> args,
|
||||
Duration requestTimeout = const Duration(seconds: 60),
|
||||
int retries = 2,
|
||||
}) async {
|
||||
|
@ -358,65 +396,50 @@ class ElectrumXClient {
|
|||
}
|
||||
|
||||
if (_requireMutex) {
|
||||
await _torConnectingLock.protect(() async => _checkRpcClient());
|
||||
await _torConnectingLock
|
||||
.protect(() async => await checkElectrumAdapter());
|
||||
} else {
|
||||
_checkRpcClient();
|
||||
await checkElectrumAdapter();
|
||||
}
|
||||
|
||||
try {
|
||||
final List<String> requestStrings = [];
|
||||
|
||||
for (final entry in args.entries) {
|
||||
final jsonArgs = json.encode(entry.value);
|
||||
requestStrings.add(
|
||||
'{"jsonrpc": "2.0", "id": "${entry.key}","method": "$command","params": $jsonArgs}');
|
||||
}
|
||||
|
||||
// combine request strings into json array
|
||||
String request = "[";
|
||||
for (int i = 0; i < requestStrings.length - 1; i++) {
|
||||
request += "${requestStrings[i]},";
|
||||
}
|
||||
request += "${requestStrings.last}]";
|
||||
|
||||
// Logging.instance.log("batch request: $request");
|
||||
|
||||
// send batch request
|
||||
final jsonRpcResponse =
|
||||
(await _rpcClient!.request(request, requestTimeout));
|
||||
|
||||
if (jsonRpcResponse.exception != null) {
|
||||
throw jsonRpcResponse.exception!;
|
||||
}
|
||||
|
||||
final List<dynamic> response;
|
||||
try {
|
||||
response = jsonRpcResponse.data as List;
|
||||
} catch (_) {
|
||||
throw Exception(
|
||||
"Expected json list but got a map: ${jsonRpcResponse.data}",
|
||||
);
|
||||
}
|
||||
|
||||
// check for errors, format and throw if there are any
|
||||
final List<String> errors = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
final result = response[i];
|
||||
if (result["error"] != null || result["result"] == null) {
|
||||
errors.add(result.toString());
|
||||
var futures = <Future<dynamic>>[];
|
||||
_electrumAdapterClient!.peer.withBatch(() {
|
||||
for (final arg in args) {
|
||||
futures.add(_electrumAdapterClient!.request(command, arg));
|
||||
}
|
||||
}
|
||||
if (errors.isNotEmpty) {
|
||||
String error = "[\n";
|
||||
for (int i = 0; i < errors.length; i++) {
|
||||
error += "${errors[i]}\n";
|
||||
}
|
||||
error += "]";
|
||||
throw Exception("JSONRPC response error: $error");
|
||||
}
|
||||
});
|
||||
final response = await Future.wait(futures);
|
||||
|
||||
// We cannot modify the response list as the order and length are related
|
||||
// to the order and length of the batched requests!
|
||||
//
|
||||
// // check for errors, format and throw if there are any
|
||||
// final List<String> errors = [];
|
||||
// for (int i = 0; i < response.length; i++) {
|
||||
// var result = response[i];
|
||||
//
|
||||
// if (result == null || (result is List && result.isEmpty)) {
|
||||
// continue;
|
||||
// // TODO [prio=extreme]: Figure out if this is actually an issue.
|
||||
// }
|
||||
// result = result[0]; // Unwrap the list.
|
||||
// if ((result is Map && result.keys.contains("error")) ||
|
||||
// result == null) {
|
||||
// errors.add(result.toString());
|
||||
// }
|
||||
// }
|
||||
// if (errors.isNotEmpty) {
|
||||
// String error = "[\n";
|
||||
// for (int i = 0; i < errors.length; i++) {
|
||||
// error += "${errors[i]}\n";
|
||||
// }
|
||||
// error += "]";
|
||||
// throw Exception("JSONRPC response error: $error");
|
||||
// }
|
||||
|
||||
currentFailoverIndex = -1;
|
||||
return List<Map<String, dynamic>>.from(response, growable: false);
|
||||
return response;
|
||||
} on WifiOnlyException {
|
||||
rethrow;
|
||||
} on SocketException {
|
||||
|
@ -451,13 +474,23 @@ class ElectrumXClient {
|
|||
/// Returns true if ping succeeded
|
||||
Future<bool> ping({String? requestID, int retryCount = 1}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
// This doesn't work because electrum_adapter only returns the result:
|
||||
// (which is always `null`).
|
||||
// await checkElectrumAdapter();
|
||||
// final response = await electrumAdapterClient!
|
||||
// .ping()
|
||||
// .timeout(const Duration(seconds: 2));
|
||||
// return (response as Map<String, dynamic>).isNotEmpty;
|
||||
|
||||
// Because request() has been updated to use electrum_adapter, and because
|
||||
// electrum_adapter returns the result of the request, request() has been
|
||||
// updated to return a bool on a server.ping command as a special case.
|
||||
return await request(
|
||||
requestID: requestID,
|
||||
command: 'server.ping',
|
||||
requestTimeout: const Duration(seconds: 2),
|
||||
retries: retryCount,
|
||||
).timeout(const Duration(seconds: 2)) as Map<String, dynamic>;
|
||||
return response.keys.contains("result") && response["result"] == null;
|
||||
).timeout(const Duration(seconds: 2)) as bool;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -478,14 +511,14 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'blockchain.headers.subscribe',
|
||||
);
|
||||
if (response["result"] == null) {
|
||||
if (response == null) {
|
||||
Logging.instance.log(
|
||||
"getBlockHeadTip returned null response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
throw 'getBlockHeadTip returned null response';
|
||||
}
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -510,7 +543,7 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'server.features',
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -531,7 +564,7 @@ class ElectrumXClient {
|
|||
rawTx,
|
||||
],
|
||||
);
|
||||
return response["result"] as String;
|
||||
return response as String;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -558,7 +591,7 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -595,8 +628,7 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
|
||||
result = response["result"];
|
||||
result = response;
|
||||
retryCount--;
|
||||
}
|
||||
|
||||
|
@ -606,17 +638,17 @@ class ElectrumXClient {
|
|||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getBatchHistory(
|
||||
{required Map<String, List<dynamic>> args}) async {
|
||||
Future<List<List<Map<String, dynamic>>>> getBatchHistory({
|
||||
required List<dynamic> args,
|
||||
}) async {
|
||||
try {
|
||||
final response = await batchRequest(
|
||||
command: 'blockchain.scripthash.get_history',
|
||||
args: args,
|
||||
);
|
||||
final Map<String, List<Map<String, dynamic>>> result = {};
|
||||
final List<List<Map<String, dynamic>>> result = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
result[response[i]["id"] as String] =
|
||||
List<Map<String, dynamic>>.from(response[i]["result"] as List);
|
||||
result.add(List<Map<String, dynamic>>.from(response[i] as List));
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@ -654,23 +686,37 @@ class ElectrumXClient {
|
|||
scripthash,
|
||||
],
|
||||
);
|
||||
return List<Map<String, dynamic>>.from(response["result"] as List);
|
||||
return List<Map<String, dynamic>>.from(response as List);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, List<Map<String, dynamic>>>> getBatchUTXOs(
|
||||
{required Map<String, List<dynamic>> args}) async {
|
||||
Future<List<List<Map<String, dynamic>>>> getBatchUTXOs({
|
||||
required List<dynamic> args,
|
||||
}) async {
|
||||
try {
|
||||
final response = await batchRequest(
|
||||
command: 'blockchain.scripthash.listunspent',
|
||||
args: args,
|
||||
);
|
||||
final Map<String, List<Map<String, dynamic>>> result = {};
|
||||
final List<List<Map<String, dynamic>>> result = [];
|
||||
for (int i = 0; i < response.length; i++) {
|
||||
result[response[i]["id"] as String] =
|
||||
List<Map<String, dynamic>>.from(response[i]["result"] as List);
|
||||
if ((response[i] as List).isNotEmpty) {
|
||||
try {
|
||||
final data = List<Map<String, dynamic>>.from(response[i] as List);
|
||||
result.add(data);
|
||||
} catch (e) {
|
||||
// to ensure we keep same length of responses as requests/args
|
||||
// add empty list on error
|
||||
result.add([]);
|
||||
|
||||
Logging.instance.log(
|
||||
"getBatchUTXOs failed to parse response=${response[i]}: $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@ -730,36 +776,18 @@ class ElectrumXClient {
|
|||
bool verbose = true,
|
||||
String? requestID,
|
||||
}) async {
|
||||
dynamic response;
|
||||
try {
|
||||
response = await request(
|
||||
requestID: requestID,
|
||||
command: 'blockchain.transaction.get',
|
||||
args: [
|
||||
txHash,
|
||||
verbose,
|
||||
],
|
||||
);
|
||||
if (!verbose) {
|
||||
return {"rawtx": response["result"] as String};
|
||||
}
|
||||
Logging.instance.log("attempting to fetch blockchain.transaction.get...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
dynamic response = await _electrumAdapterClient!.getTransaction(txHash);
|
||||
Logging.instance.log("Fetching blockchain.transaction.get finished",
|
||||
level: LogLevel.Info);
|
||||
|
||||
if (response["result"] == null) {
|
||||
Logging.instance.log(
|
||||
"getTransaction($txHash) returned null response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
throw 'getTransaction($txHash) returned null response';
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"getTransaction($txHash) response: $response",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
if (!verbose) {
|
||||
return {"rawtx": response as String};
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
}
|
||||
|
||||
/// Returns the whole Lelantus anonymity set for denomination in the groupId.
|
||||
|
@ -781,23 +809,15 @@ class ElectrumXClient {
|
|||
String blockhash = "",
|
||||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getanonymityset',
|
||||
args: [
|
||||
groupId,
|
||||
blockhash,
|
||||
],
|
||||
);
|
||||
Logging.instance.log("Fetching lelantus.getanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash);
|
||||
Logging.instance.log("Fetching lelantus.getanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
//TODO add example to docs
|
||||
|
@ -808,18 +828,14 @@ class ElectrumXClient {
|
|||
dynamic mints,
|
||||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getmintmetadata',
|
||||
args: [
|
||||
mints,
|
||||
],
|
||||
);
|
||||
return response["result"];
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getmintmetadata...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
dynamic response = await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusMintData(mints: mints);
|
||||
Logging.instance.log("Fetching lelantus.getmintmetadata finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
//TODO add example to docs
|
||||
|
@ -828,45 +844,38 @@ class ElectrumXClient {
|
|||
String? requestID,
|
||||
required int startNumber,
|
||||
}) async {
|
||||
try {
|
||||
int retryCount = 3;
|
||||
dynamic result;
|
||||
Logging.instance.log("attempting to fetch lelantus.getusedcoinserials...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
|
||||
while (retryCount > 0 && result is! List) {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getusedcoinserials',
|
||||
args: [
|
||||
"$startNumber",
|
||||
],
|
||||
requestTimeout: const Duration(minutes: 2),
|
||||
);
|
||||
int retryCount = 3;
|
||||
dynamic response;
|
||||
|
||||
result = response["result"];
|
||||
retryCount--;
|
||||
}
|
||||
while (retryCount > 0 && response is! List) {
|
||||
response = await (_electrumAdapterClient as FiroElectrumClient)!
|
||||
.getLelantusUsedCoinSerials(startNumber: startNumber);
|
||||
// TODO add 2 minute timeout.
|
||||
Logging.instance.log("Fetching lelantus.getusedcoinserials finished",
|
||||
level: LogLevel.Info);
|
||||
|
||||
return Map<String, dynamic>.from(result as Map);
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
retryCount--;
|
||||
}
|
||||
|
||||
return Map<String, dynamic>.from(response as Map);
|
||||
}
|
||||
|
||||
/// Returns the latest Lelantus set id
|
||||
///
|
||||
/// ex: 1
|
||||
Future<int> getLelantusLatestCoinId({String? requestID}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'lelantus.getlatestcoinid',
|
||||
);
|
||||
return response["result"] as int;
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
Logging.instance.log("attempting to fetch lelantus.getlatestcoinid...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
int response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient).getLatestCoinId();
|
||||
Logging.instance.log("Fetching lelantus.getlatestcoinid finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ============== Spark ======================================================
|
||||
|
@ -892,17 +901,14 @@ class ElectrumXClient {
|
|||
try {
|
||||
Logging.instance.log("attempting to fetch spark.getsparkanonymityset...",
|
||||
level: LogLevel.Info);
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparkanonymityset',
|
||||
args: [
|
||||
coinGroupId,
|
||||
startBlockHash,
|
||||
],
|
||||
);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkAnonymitySet(
|
||||
coinGroupId: coinGroupId, startBlockHash: startBlockHash);
|
||||
Logging.instance.log("Fetching spark.getsparkanonymityset finished",
|
||||
level: LogLevel.Info);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
return response;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -915,15 +921,17 @@ class ElectrumXClient {
|
|||
required int startNumber,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getusedcoinstags',
|
||||
args: [
|
||||
"$startNumber",
|
||||
],
|
||||
requestTimeout: const Duration(minutes: 2),
|
||||
);
|
||||
final map = Map<String, dynamic>.from(response["result"] as Map);
|
||||
// Use electrum_adapter package's getSparkUsedCoinsTags method.
|
||||
Logging.instance.log("attempting to fetch spark.getusedcoinstags...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
Map<String, dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getUsedCoinsTags(startNumber: startNumber);
|
||||
// TODO: Add 2 minute timeout.
|
||||
Logging.instance.log("Fetching spark.getusedcoinstags finished",
|
||||
level: LogLevel.Info);
|
||||
final map = Map<String, dynamic>.from(response);
|
||||
final set = Set<String>.from(map["tags"] as List);
|
||||
return await compute(_ffiHashTagsComputeWrapper, set);
|
||||
} catch (e) {
|
||||
|
@ -947,16 +955,15 @@ class ElectrumXClient {
|
|||
required List<String> sparkCoinHashes,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparkmintmetadata',
|
||||
args: [
|
||||
{
|
||||
"coinHashes": sparkCoinHashes,
|
||||
},
|
||||
],
|
||||
);
|
||||
return List<Map<String, dynamic>>.from(response["result"] as List);
|
||||
Logging.instance.log("attempting to fetch spark.getsparkmintmetadata...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
List<dynamic> response =
|
||||
await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkMintMetaData(sparkCoinHashes: sparkCoinHashes);
|
||||
Logging.instance.log("Fetching spark.getsparkmintmetadata finished",
|
||||
level: LogLevel.Info);
|
||||
return List<Map<String, dynamic>>.from(response);
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
|
@ -970,11 +977,14 @@ class ElectrumXClient {
|
|||
String? requestID,
|
||||
}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'spark.getsparklatestcoinid',
|
||||
);
|
||||
return response["result"] as int;
|
||||
Logging.instance.log("attempting to fetch spark.getsparklatestcoinid...",
|
||||
level: LogLevel.Info);
|
||||
await checkElectrumAdapter();
|
||||
int response = await (_electrumAdapterClient as FiroElectrumClient)
|
||||
.getSparkLatestCoinId();
|
||||
Logging.instance.log("Fetching spark.getsparklatestcoinid finished",
|
||||
level: LogLevel.Info);
|
||||
return response;
|
||||
} catch (e) {
|
||||
Logging.instance.log(e, level: LogLevel.Error);
|
||||
rethrow;
|
||||
|
@ -991,15 +1001,8 @@ class ElectrumXClient {
|
|||
/// "rate": 1000,
|
||||
/// }
|
||||
Future<Map<String, dynamic>> getFeeRate({String? requestID}) async {
|
||||
try {
|
||||
final response = await request(
|
||||
requestID: requestID,
|
||||
command: 'blockchain.getfeerate',
|
||||
);
|
||||
return Map<String, dynamic>.from(response["result"] as Map);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
await checkElectrumAdapter();
|
||||
return await _electrumAdapterClient!.getFeeRate();
|
||||
}
|
||||
|
||||
/// Return the estimated transaction fee per kilobyte for a transaction to be confirmed within a certain number of [blocks].
|
||||
|
@ -1016,7 +1019,26 @@ class ElectrumXClient {
|
|||
blocks,
|
||||
],
|
||||
);
|
||||
return Decimal.parse(response["result"].toString());
|
||||
try {
|
||||
// If the response is -1 or null, return a temporary hardcoded value for
|
||||
// Dogecoin. This is a temporary fix until the fee estimation is fixed.
|
||||
if (coin == Coin.dogecoin &&
|
||||
(response == null ||
|
||||
response == -1 ||
|
||||
Decimal.parse(response.toString()) == Decimal.parse("-1"))) {
|
||||
// Return 0.05 for slow, 0.2 for average, and 1 for fast txs.
|
||||
// These numbers produce tx fees in line with txs in the wild on
|
||||
// https://dogechain.info/
|
||||
return Decimal.parse((1 / blocks).toString());
|
||||
// TODO [prio=med]: Fix fee estimation.
|
||||
}
|
||||
return Decimal.parse(response.toString());
|
||||
} catch (e, s) {
|
||||
final String msg = "Error parsing fee rate. Response: $response"
|
||||
"\nResult: ${response}\nError: $e\nStack trace: $s";
|
||||
Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
throw Exception(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -1033,7 +1055,7 @@ class ElectrumXClient {
|
|||
requestID: requestID,
|
||||
command: 'blockchain.relayfee',
|
||||
);
|
||||
return Decimal.parse(response["result"].toString());
|
||||
return Decimal.parse(response.toString());
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
|
|
@ -80,18 +80,32 @@ class JsonRPC {
|
|||
void _sendNextAvailableRequest() {
|
||||
_requestQueue.nextIncompleteReq.then((req) {
|
||||
if (req != null) {
|
||||
// \r\n required by electrumx server
|
||||
if (_socket != null) {
|
||||
if (!Prefs.instance.useTor) {
|
||||
if (_socket == null) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC _sendNextAvailableRequest attempted with"
|
||||
" _socket=null on $host:$port",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
// \r\n required by electrumx server
|
||||
_socket!.write('${req.jsonRequest}\r\n');
|
||||
}
|
||||
if (_socksSocket != null) {
|
||||
_socksSocket!.write('${req.jsonRequest}\r\n');
|
||||
} else {
|
||||
if (_socksSocket == null) {
|
||||
Logging.instance.log(
|
||||
"JsonRPC _sendNextAvailableRequest attempted with"
|
||||
" _socksSocket=null on $host:$port",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
// \r\n required by electrumx server
|
||||
_socksSocket?.write('${req.jsonRequest}\r\n');
|
||||
}
|
||||
|
||||
// TODO different timeout length?
|
||||
req.initiateTimeout(
|
||||
onTimedOut: () {
|
||||
_requestQueue.remove(req);
|
||||
_onReqCompleted(req);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -109,7 +123,7 @@ class JsonRPC {
|
|||
"JsonRPC request: opening socket $host:$port",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
await connect().timeout(requestTimeout, onTimeout: () {
|
||||
await _connect().timeout(requestTimeout, onTimeout: () {
|
||||
throw Exception("Request timeout: $jsonRpcRequest");
|
||||
});
|
||||
}
|
||||
|
@ -119,7 +133,7 @@ class JsonRPC {
|
|||
"JsonRPC request: opening SOCKS socket to $host:$port",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
await connect().timeout(requestTimeout, onTimeout: () {
|
||||
await _connect().timeout(requestTimeout, onTimeout: () {
|
||||
throw Exception("Request timeout: $jsonRpcRequest");
|
||||
});
|
||||
}
|
||||
|
@ -156,23 +170,42 @@ class JsonRPC {
|
|||
return future;
|
||||
}
|
||||
|
||||
Future<void> disconnect({required String reason}) async {
|
||||
await _requestMutex.protect(() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
await _socksSocket?.close();
|
||||
_socksSocket = null;
|
||||
|
||||
// clean up remaining queue
|
||||
await _requestQueue.completeRemainingWithError(
|
||||
"JsonRPC disconnect() called with reason: \"$reason\"",
|
||||
);
|
||||
});
|
||||
/// DO NOT set [ignoreMutex] to true unless fully aware of the consequences
|
||||
Future<void> disconnect({
|
||||
required String reason,
|
||||
bool ignoreMutex = false,
|
||||
}) async {
|
||||
if (ignoreMutex) {
|
||||
await _disconnectHelper(reason: reason);
|
||||
} else {
|
||||
await _requestMutex.protect(() async {
|
||||
await _disconnectHelper(reason: reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
Future<void> _disconnectHelper({required String reason}) async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
await _socksSocket?.close();
|
||||
_socksSocket = null;
|
||||
|
||||
// clean up remaining queue
|
||||
await _requestQueue.completeRemainingWithError(
|
||||
"JsonRPC disconnect() called with reason: \"$reason\"",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _connect() async {
|
||||
// ignore mutex is set to true here as _connect is already called within
|
||||
// the mutex.protect block. Setting to false here leads to a deadlock
|
||||
await disconnect(
|
||||
reason: "New connection requested",
|
||||
ignoreMutex: true,
|
||||
);
|
||||
|
||||
if (!Prefs.instance.useTor) {
|
||||
if (useSSL) {
|
||||
_socket = await SecureSocket.connect(
|
||||
|
@ -180,7 +213,7 @@ class JsonRPC {
|
|||
port,
|
||||
timeout: connectionTimeout,
|
||||
onBadCertificate: (_) => true,
|
||||
); // TODO do not automatically trust bad certificates
|
||||
); // TODO do not automatically trust bad certificates.
|
||||
} else {
|
||||
_socket = await Socket.connect(
|
||||
host,
|
||||
|
@ -352,17 +385,20 @@ class _JsonRPCRequest {
|
|||
}
|
||||
|
||||
void initiateTimeout({
|
||||
VoidCallback? onTimedOut,
|
||||
required VoidCallback onTimedOut,
|
||||
}) {
|
||||
Future<void>.delayed(requestTimeout).then((_) {
|
||||
if (!isComplete) {
|
||||
try {
|
||||
throw JsonRpcException("_JsonRPCRequest timed out: $jsonRequest");
|
||||
} catch (e, s) {
|
||||
completer.completeError(e, s);
|
||||
onTimedOut?.call();
|
||||
}
|
||||
completer.complete(
|
||||
JsonRPCResponse(
|
||||
data: null,
|
||||
exception: JsonRpcException(
|
||||
"_JsonRPCRequest timed out: $jsonRequest",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
onTimedOut.call();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -375,14 +411,3 @@ class JsonRPCResponse {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,16 +12,25 @@
|
|||
// import 'dart:convert';
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter/foundation.dart';
|
||||
// import 'package:event_bus/event_bus.dart';
|
||||
// import 'package:mutex/mutex.dart';
|
||||
// import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
|
||||
// import 'package:stackwallet/exceptions/json_rpc/json_rpc_exception.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:tor_ffi_plugin/socks_socket.dart';
|
||||
//
|
||||
// class ElectrumXSubscription with ChangeNotifier {
|
||||
// dynamic _response;
|
||||
// dynamic get response => _response;
|
||||
// set response(dynamic newData) {
|
||||
// _response = newData;
|
||||
// notifyListeners();
|
||||
// }
|
||||
// class ElectrumXSubscription {
|
||||
// final StreamController<dynamic> _controller =
|
||||
// StreamController(); // TODO controller params
|
||||
//
|
||||
// Stream<dynamic> get responseStream => _controller.stream;
|
||||
//
|
||||
// void addToStream(dynamic data) => _controller.add(data);
|
||||
// }
|
||||
//
|
||||
// class SocketTask {
|
||||
|
@ -40,67 +49,430 @@
|
|||
// final Map<String, SocketTask> _tasks = {};
|
||||
// Timer? _aliveTimer;
|
||||
// Socket? _socket;
|
||||
// SOCKSSocket? _socksSocket;
|
||||
// late final bool _useSSL;
|
||||
// late final Duration _connectionTimeout;
|
||||
// late final Duration _keepAlive;
|
||||
//
|
||||
// bool get isConnected => _isConnected;
|
||||
// bool get useSSL => _useSSL;
|
||||
// // Used to reconnect.
|
||||
// String? _host;
|
||||
// int? _port;
|
||||
//
|
||||
// void Function(bool)? onConnectionStatusChanged;
|
||||
//
|
||||
// late Prefs _prefs;
|
||||
// late TorService _torService;
|
||||
// StreamSubscription<TorPreferenceChangedEvent>? _torPreferenceListener;
|
||||
// StreamSubscription<TorConnectionStatusChangedEvent>? _torStatusListener;
|
||||
// final Mutex _torConnectingLock = Mutex();
|
||||
// bool _requireMutex = false;
|
||||
//
|
||||
// List<ElectrumXNode>? failovers;
|
||||
// int currentFailoverIndex = -1;
|
||||
//
|
||||
// SubscribableElectrumXClient({
|
||||
// bool useSSL = true,
|
||||
// required bool useSSL,
|
||||
// required Prefs prefs,
|
||||
// required List<ElectrumXNode> failovers,
|
||||
// TorService? torService,
|
||||
// this.onConnectionStatusChanged,
|
||||
// Duration connectionTimeout = const Duration(seconds: 5),
|
||||
// Duration keepAlive = const Duration(seconds: 10),
|
||||
// EventBus? globalEventBusForTesting,
|
||||
// }) {
|
||||
// _useSSL = useSSL;
|
||||
// _prefs = prefs;
|
||||
// _torService = torService ?? TorService.sharedInstance;
|
||||
// _connectionTimeout = connectionTimeout;
|
||||
// _keepAlive = keepAlive;
|
||||
//
|
||||
// // If we're testing, use the global event bus for testing.
|
||||
// final bus = globalEventBusForTesting ?? GlobalEventBus.instance;
|
||||
//
|
||||
// // Listen to global event bus for Tor status changes.
|
||||
// _torStatusListener = bus.on<TorConnectionStatusChangedEvent>().listen(
|
||||
// (event) async {
|
||||
// try {
|
||||
// switch (event.newStatus) {
|
||||
// case TorConnectionStatus.connecting:
|
||||
// // If Tor is connecting, we need to wait.
|
||||
// await _torConnectingLock.acquire();
|
||||
// _requireMutex = true;
|
||||
// break;
|
||||
//
|
||||
// case TorConnectionStatus.connected:
|
||||
// case TorConnectionStatus.disconnected:
|
||||
// // If Tor is connected or disconnected, we can release the lock.
|
||||
// if (_torConnectingLock.isLocked) {
|
||||
// _torConnectingLock.release();
|
||||
// }
|
||||
// _requireMutex = false;
|
||||
// break;
|
||||
// }
|
||||
// } finally {
|
||||
// // Ensure the lock is released.
|
||||
// if (_torConnectingLock.isLocked) {
|
||||
// _torConnectingLock.release();
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
//
|
||||
// // Listen to global event bus for Tor preference changes.
|
||||
// _torPreferenceListener = bus.on<TorPreferenceChangedEvent>().listen(
|
||||
// (event) async {
|
||||
// // Close open socket (if open).
|
||||
// final tempSocket = _socket;
|
||||
// _socket = null;
|
||||
// await tempSocket?.close();
|
||||
//
|
||||
// // Close open SOCKS socket (if open).
|
||||
// final tempSOCKSSocket = _socksSocket;
|
||||
// _socksSocket = null;
|
||||
// await tempSOCKSSocket?.close();
|
||||
//
|
||||
// // Clear subscriptions.
|
||||
// _tasks.clear();
|
||||
//
|
||||
// // Cancel alive timer
|
||||
// _aliveTimer?.cancel();
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future<void> connect({required String host, required int port}) async {
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (_) {}
|
||||
// factory SubscribableElectrumXClient.from({
|
||||
// required ElectrumXNode node,
|
||||
// required Prefs prefs,
|
||||
// required List<ElectrumXNode> failovers,
|
||||
// TorService? torService,
|
||||
// }) {
|
||||
// return SubscribableElectrumXClient(
|
||||
// useSSL: node.useSSL,
|
||||
// prefs: prefs,
|
||||
// failovers: failovers,
|
||||
// torService: torService ?? TorService.sharedInstance,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// if (_useSSL) {
|
||||
// _socket = await SecureSocket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// onBadCertificate: (_) => true,
|
||||
// );
|
||||
// } else {
|
||||
// _socket = await Socket.connect(
|
||||
// host,
|
||||
// port,
|
||||
// timeout: _connectionTimeout,
|
||||
// );
|
||||
// // Example for returning a future which completes upon connection.
|
||||
// // static Future<SubscribableElectrumXClient> from({
|
||||
// // required ElectrumXNode node,
|
||||
// // TorService? torService,
|
||||
// // }) async {
|
||||
// // final client = SubscribableElectrumXClient(
|
||||
// // useSSL: node.useSSL,
|
||||
// // );
|
||||
// //
|
||||
// // await client.connect(host: node.address, port: node.port);
|
||||
// //
|
||||
// // return client;
|
||||
// // }
|
||||
//
|
||||
// /// Check if the RPC client is connected and connect if needed.
|
||||
// ///
|
||||
// /// If Tor is enabled but not running, it will attempt to start Tor.
|
||||
// Future<void> _checkSocket({bool connecting = false}) async {
|
||||
// if (_prefs.useTor) {
|
||||
// // If we're supposed to use Tor...
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // ... but Tor isn't running...
|
||||
// if (!_prefs.torKillSwitch) {
|
||||
// // ... and the killswitch isn't set, then we'll just return below.
|
||||
// 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 let's try to start Tor.
|
||||
// await _torService.start();
|
||||
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
|
||||
//
|
||||
// // Double-check that Tor is running.
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // If Tor still isn't running, then we'll throw an exception.
|
||||
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
|
||||
// "Tor preference and killswitch set but Tor not enabled and could not start, not connecting to ElectrumX.");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _updateConnectionStatus(true);
|
||||
//
|
||||
// _socket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// // Connect if needed.
|
||||
// if (!connecting) {
|
||||
// if ((!_prefs.useTor && _socket == null) ||
|
||||
// (_prefs.useTor && _socksSocket == null)) {
|
||||
// if (currentFailoverIndex == -1) {
|
||||
// // Check if we have cached node information
|
||||
// if (_host == null && _port == null) {
|
||||
// throw Exception("SubscribableElectrumXClient._checkRpcClient: "
|
||||
// "No host or port provided and no cached node information.");
|
||||
// }
|
||||
//
|
||||
// _aliveTimer?.cancel();
|
||||
// _aliveTimer = Timer.periodic(
|
||||
// _keepAlive,
|
||||
// (_) async => _updateConnectionStatus(await ping()),
|
||||
// );
|
||||
// // Connect to the server.
|
||||
// await connect(host: _host!, port: _port!);
|
||||
// } else {
|
||||
// // Attempt to connect to the next failover server.
|
||||
// await connect(
|
||||
// host: failovers![currentFailoverIndex].address,
|
||||
// port: failovers![currentFailoverIndex].port,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server.
|
||||
// ///
|
||||
// /// If Tor is enabled, it will attempt to connect through Tor.
|
||||
// Future<void> connect({
|
||||
// required String host,
|
||||
// required int port,
|
||||
// }) async {
|
||||
// try {
|
||||
// // Cache node information.
|
||||
// _host = host;
|
||||
// _port = port;
|
||||
//
|
||||
// // If we're already connected, disconnect first.
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (_) {}
|
||||
//
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock
|
||||
// .protect(() async => await _checkSocket(connecting: true));
|
||||
// } else {
|
||||
// await _checkSocket(connecting: true);
|
||||
// }
|
||||
//
|
||||
// if (!Prefs.instance.useTor) {
|
||||
// // If we're not supposed to use Tor, then connect directly.
|
||||
// await connectClearnet(host, port);
|
||||
// } else {
|
||||
// // If we're supposed to use Tor...
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // ... but Tor isn't running...
|
||||
// if (!_prefs.torKillSwitch) {
|
||||
// // ... and the killswitch isn't set, then we'll connect clearnet.
|
||||
// Logging.instance.log(
|
||||
// "Tor preference set but Tor not enabled, no killswitch set, connecting to ElectrumX through clearnet",
|
||||
// level: LogLevel.Warning,
|
||||
// );
|
||||
// await connectClearnet(host, port);
|
||||
// } else {
|
||||
// // ... but if the killswitch is set, then let's try to start Tor.
|
||||
// await _torService.start();
|
||||
// // TODO [prio=low]: Attempt to restart Tor if needed. Update Tor package for restart feature.
|
||||
//
|
||||
// // Doublecheck that Tor is running.
|
||||
// if (_torService.status != TorConnectionStatus.connected) {
|
||||
// // If Tor still isn't running, then we'll throw an exception.
|
||||
// throw Exception(
|
||||
// "Tor preference and killswitch set but Tor not enabled, not connecting to ElectrumX");
|
||||
// }
|
||||
//
|
||||
// // Connect via Tor.
|
||||
// await connectTor(host, port);
|
||||
// }
|
||||
// } else {
|
||||
// // Connect via Tor.
|
||||
// await connectTor(host, port);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// _updateConnectionStatus(true);
|
||||
//
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final String msg = "SubscribableElectrumXClient.connect(): "
|
||||
// "cannot listen to $host:$port via SOCKSSocket because it is not connected.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
//
|
||||
// _socksSocket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final String msg = "SubscribableElectrumXClient.connect(): "
|
||||
// "cannot listen to $host:$port via socket because it is not connected.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
//
|
||||
// _socket!.listen(
|
||||
// _dataHandler,
|
||||
// onError: _errorHandler,
|
||||
// onDone: _doneHandler,
|
||||
// cancelOnError: true,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// _aliveTimer?.cancel();
|
||||
// _aliveTimer = Timer.periodic(
|
||||
// _keepAlive,
|
||||
// (_) async => _updateConnectionStatus(await ping()),
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient.connect: "
|
||||
// "failed to connect to $host:$port."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
//
|
||||
// // Ensure cleanup is performed on failure to avoid resource leaks.
|
||||
// await disconnect(); // Use the disconnect method to clean up.
|
||||
// rethrow; // Rethrow the exception to handle it further up the call stack.
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server directly.
|
||||
// Future<void> connectClearnet(String host, int port) async {
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectClearnet(): "
|
||||
// "creating a socket to $host:$port (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// 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,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectClearnet(): "
|
||||
// "created socket to $host:$port...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectClearnet: "
|
||||
// "failed to connect to $host (SSL: $useSSL)."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// /// Connect to the server using the Tor service.
|
||||
// Future<void> connectTor(String host, int port) async {
|
||||
// // Get the proxy info from the TorService.
|
||||
// final proxyInfo = _torService.getProxyInfo();
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "creating a SOCKS socket at $proxyInfo (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// // Create a socks socket using the Tor service's proxy info.
|
||||
// _socksSocket = await SOCKSSocket.create(
|
||||
// proxyHost: proxyInfo.host.address,
|
||||
// proxyPort: proxyInfo.port,
|
||||
// sslEnabled: useSSL,
|
||||
// );
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "created SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to create a SOCKS socket at $proxyInfo (SSL $useSSL)..."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// await _socksSocket?.connect();
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connected to SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to connect to SOCKS socket at $proxyInfo.."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connecting to $host:$port over SOCKS socket at $proxyInfo...",
|
||||
// level: LogLevel.Info);
|
||||
//
|
||||
// await _socksSocket?.connectTo(host, port);
|
||||
//
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.connectTor(): "
|
||||
// "connected to $host:$port over SOCKS socket at $proxyInfo",
|
||||
// level: LogLevel.Info);
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient.connectTor(): "
|
||||
// "failed to connect $host over tor proxy at $proxyInfo."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// /// Disconnect from the server.
|
||||
// Future<void> disconnect() async {
|
||||
// _aliveTimer?.cancel();
|
||||
// await _socket?.close();
|
||||
// _aliveTimer = null;
|
||||
//
|
||||
// try {
|
||||
// await _socket?.close();
|
||||
// } catch (e, s) {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.disconnect: failed to close socket."
|
||||
// "\nError: $e\nStack trace: $s",
|
||||
// level: LogLevel.Warning);
|
||||
// }
|
||||
// _socket = null;
|
||||
//
|
||||
// try {
|
||||
// await _socksSocket?.close();
|
||||
// } catch (e, s) {
|
||||
// Logging.instance.log(
|
||||
// "SubscribableElectrumXClient.disconnect: failed to close SOCKS socket."
|
||||
// "\nError: $e\nStack trace: $s",
|
||||
// level: LogLevel.Warning);
|
||||
// }
|
||||
// _socksSocket = null;
|
||||
//
|
||||
// onConnectionStatusChanged = null;
|
||||
// }
|
||||
//
|
||||
// /// Format JSON request string.
|
||||
// String _buildJsonRequestString({
|
||||
// required String method,
|
||||
// required String id,
|
||||
|
@ -110,6 +482,7 @@
|
|||
// return '{"jsonrpc": "2.0", "id": "$id","method": "$method","params": $paramString}\r\n';
|
||||
// }
|
||||
//
|
||||
// /// Update the connection status and call the onConnectionStatusChanged callback if it exists.
|
||||
// void _updateConnectionStatus(bool connectionStatus) {
|
||||
// if (_isConnected != connectionStatus && onConnectionStatusChanged != null) {
|
||||
// onConnectionStatusChanged!(connectionStatus);
|
||||
|
@ -117,6 +490,7 @@
|
|||
// _isConnected = connectionStatus;
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has data.
|
||||
// void _dataHandler(List<int> data) {
|
||||
// _responseData.addAll(data);
|
||||
//
|
||||
|
@ -137,6 +511,7 @@
|
|||
// }
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has a response.
|
||||
// void _responseHandler(Map<String, dynamic> response) {
|
||||
// // subscriptions will have a method in the response
|
||||
// if (response['method'] is String) {
|
||||
|
@ -150,6 +525,7 @@
|
|||
// _complete(id, result);
|
||||
// }
|
||||
//
|
||||
// /// Called when the subscription has a response.
|
||||
// void _subscriptionHandler({
|
||||
// required Map<String, dynamic> response,
|
||||
// }) {
|
||||
|
@ -160,19 +536,20 @@
|
|||
// final scripthash = params.first as String;
|
||||
// final taskId = "blockchain.scripthash.subscribe:$scripthash";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.last;
|
||||
// _tasks[taskId]?.subscription?.addToStream(params.last);
|
||||
// break;
|
||||
// case "blockchain.headers.subscribe":
|
||||
// final params = response["params"];
|
||||
// const taskId = "blockchain.headers.subscribe";
|
||||
//
|
||||
// _tasks[taskId]?.subscription?.response = params.first;
|
||||
// _tasks[taskId]?.subscription?.addToStream(params.first);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket has an error.
|
||||
// void _errorHandler(Object error, StackTrace trace) {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log(
|
||||
|
@ -180,12 +557,14 @@
|
|||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// /// Called when the socket is closed.
|
||||
// void _doneHandler() {
|
||||
// _updateConnectionStatus(false);
|
||||
// Logging.instance.log("SubscribableElectrumXClient called _doneHandler",
|
||||
// level: LogLevel.Info);
|
||||
// }
|
||||
//
|
||||
// /// Complete a task with the given id and data.
|
||||
// void _complete(String id, dynamic data) {
|
||||
// if (_tasks[id] == null) {
|
||||
// return;
|
||||
|
@ -198,10 +577,11 @@
|
|||
// if (!(_tasks[id]?.isSubscription ?? false)) {
|
||||
// _tasks.remove(id);
|
||||
// } else {
|
||||
// _tasks[id]?.subscription?.response = data;
|
||||
// _tasks[id]?.subscription?.addToStream(data);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Add a task to the task list.
|
||||
// void _addTask({
|
||||
// required String id,
|
||||
// required Completer<dynamic> completer,
|
||||
|
@ -209,6 +589,7 @@
|
|||
// _tasks[id] = SocketTask(completer: completer, subscription: null);
|
||||
// }
|
||||
//
|
||||
// /// Add a subscription task to the task list.
|
||||
// void _addSubscriptionTask({
|
||||
// required String id,
|
||||
// required ElectrumXSubscription subscription,
|
||||
|
@ -216,83 +597,240 @@
|
|||
// _tasks[id] = SocketTask(completer: null, subscription: subscription);
|
||||
// }
|
||||
//
|
||||
// /// Write call to socket.
|
||||
// Future<dynamic> _call({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// }) async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "SOCKSSocket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "Socket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// return completer.future;
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return completer.future;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._call: "
|
||||
// "failed to request $method with id $id."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Write call to socket with timeout.
|
||||
// Future<dynamic> _callWithTimeout({
|
||||
// required String method,
|
||||
// List<dynamic> params = const [],
|
||||
// Duration timeout = const Duration(seconds: 2),
|
||||
// }) async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// try {
|
||||
// if (_host == null || _port == null) {
|
||||
// throw Exception("No host or port provided");
|
||||
// }
|
||||
//
|
||||
// // Attempt to conect.
|
||||
// await connect(
|
||||
// host: _host!,
|
||||
// port: _port!,
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "SOCKSSocket not connected and cannot connect. "
|
||||
// "Method $method, params $params."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// try {
|
||||
// if (_host == null || _port == null) {
|
||||
// throw Exception("No host or port provided");
|
||||
// }
|
||||
//
|
||||
// // Attempt to conect.
|
||||
// await connect(
|
||||
// host: _host!,
|
||||
// port: _port!,
|
||||
// );
|
||||
// } catch (e, s) {
|
||||
// final msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "Socket not connected and cannot connect. "
|
||||
// "Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// final completer = Completer<dynamic>();
|
||||
// _currentRequestID++;
|
||||
// final id = _currentRequestID.toString();
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// _addTask(id: id, completer: completer);
|
||||
//
|
||||
// Timer(timeout, () {
|
||||
// if (!completer.isCompleted) {
|
||||
// completer.completeError(
|
||||
// Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return completer.future;
|
||||
// Timer(timeout, () {
|
||||
// if (!completer.isCompleted) {
|
||||
// completer.completeError(
|
||||
// Exception("Request \"id: $id, method: $method\" timed out!"),
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// return completer.future;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._callWithTimeout: "
|
||||
// "failed to request $method with id $id (timeout $timeout)."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ElectrumXSubscription _subscribe({
|
||||
// required String taskId,
|
||||
// required String id,
|
||||
// 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,
|
||||
// ),
|
||||
// );
|
||||
// try {
|
||||
// final subscription = ElectrumXSubscription();
|
||||
// _addSubscriptionTask(id: id, subscription: subscription);
|
||||
// _currentRequestID++;
|
||||
//
|
||||
// return subscription;
|
||||
// // } catch (e, s) {
|
||||
// // Logging.instance.log("SubscribableElectrumXClient _subscribe: $e\n$s", level: LogLevel.Error);
|
||||
// // return null;
|
||||
// // }
|
||||
// // Check socket is connected.
|
||||
// if (_prefs.useTor) {
|
||||
// if (_socksSocket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "SOCKSSocket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// } else {
|
||||
// if (_socket == null) {
|
||||
// final msg = "SubscribableElectrumXClient._call: "
|
||||
// "Socket is not connected. Method $method, params $params.";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw Exception(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Write to the socket.
|
||||
// if (_prefs.useTor) {
|
||||
// _socksSocket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// _socket?.write(
|
||||
// _buildJsonRequestString(
|
||||
// method: method,
|
||||
// id: id,
|
||||
// params: params,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// return subscription;
|
||||
// } catch (e, s) {
|
||||
// final String msg = "SubscribableElectrumXClient._subscribe: "
|
||||
// "failed to subscribe to $method with id $id."
|
||||
// "\nError: $e\nStack trace: $s";
|
||||
// Logging.instance.log(msg, level: LogLevel.Fatal);
|
||||
// throw JsonRpcException(msg);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// Ping the server to ensure it is responding
|
||||
// ///
|
||||
// /// Returns true if ping succeeded
|
||||
// Future<bool> ping() async {
|
||||
// // If we're connecting to Tor, wait.
|
||||
// if (_requireMutex) {
|
||||
// await _torConnectingLock.protect(() async => await _checkSocket());
|
||||
// } else {
|
||||
// await _checkSocket();
|
||||
// }
|
||||
//
|
||||
// // Write to the socket.
|
||||
// try {
|
||||
// final response = (await _callWithTimeout(method: "server.ping")) as Map;
|
||||
// return response.keys.contains("result") && response["result"] == null;
|
||||
|
@ -304,7 +842,7 @@
|
|||
// /// Subscribe to a scripthash to receive notifications on status changes
|
||||
// ElectrumXSubscription subscribeToScripthash({required String scripthash}) {
|
||||
// return _subscribe(
|
||||
// taskId: 'blockchain.scripthash.subscribe:$scripthash',
|
||||
// id: 'blockchain.scripthash.subscribe:$scripthash',
|
||||
// method: 'blockchain.scripthash.subscribe',
|
||||
// params: [scripthash],
|
||||
// );
|
||||
|
@ -316,7 +854,7 @@
|
|||
// ElectrumXSubscription subscribeToBlockHeaders() {
|
||||
// return _tasks["blockchain.headers.subscribe"]?.subscription ??
|
||||
// _subscribe(
|
||||
// taskId: "blockchain.headers.subscribe",
|
||||
// id: "blockchain.headers.subscribe",
|
||||
// method: "blockchain.headers.subscribe",
|
||||
// params: [],
|
||||
// );
|
||||
|
|
|
@ -539,6 +539,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme>
|
|||
break;
|
||||
case AppLifecycleState.detached:
|
||||
break;
|
||||
case AppLifecycleState.hidden:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ enum AddressType {
|
|||
spark,
|
||||
stellar,
|
||||
tezos,
|
||||
frostMS,
|
||||
;
|
||||
|
||||
String get readableName {
|
||||
|
@ -193,6 +194,8 @@ enum AddressType {
|
|||
return "Stellar";
|
||||
case AddressType.tezos:
|
||||
return "Tezos";
|
||||
case AddressType.frostMS:
|
||||
return "FrostMS";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,6 +266,7 @@ const _AddresstypeEnumValueMap = {
|
|||
'spark': 10,
|
||||
'stellar': 11,
|
||||
'tezos': 12,
|
||||
'frostMS': 13,
|
||||
};
|
||||
const _AddresstypeValueEnumMap = {
|
||||
0: AddressType.p2pkh,
|
||||
|
@ -281,6 +282,7 @@ const _AddresstypeValueEnumMap = {
|
|||
10: AddressType.spark,
|
||||
11: AddressType.stellar,
|
||||
12: AddressType.tezos,
|
||||
13: AddressType.frostMS,
|
||||
};
|
||||
|
||||
Id _addressGetId(Address object) {
|
||||
|
|
|
@ -68,7 +68,7 @@ class OutputV2 {
|
|||
scriptPubKeyHex: json["scriptPubKey"]["hex"] as String,
|
||||
scriptPubKeyAsm: json["scriptPubKey"]["asm"] as String?,
|
||||
valueStringSats: parseOutputAmountString(
|
||||
json["value"].toString(),
|
||||
json["value"] != null ? json["value"].toString(): "0",
|
||||
decimalPlaces: decimalPlaces,
|
||||
isFullAmountNotSats: isFullAmountNotSats,
|
||||
),
|
||||
|
|
|
@ -134,9 +134,18 @@ class _AddWalletViewState extends ConsumerState<AddWalletView> {
|
|||
_coins.remove(Coin.wownero);
|
||||
}
|
||||
|
||||
// Remove FROST from the list of coins based on our frostEnabled preference.
|
||||
if (!ref.read(prefsChangeNotifierProvider).frostEnabled) {
|
||||
_coins.remove(Coin.bitcoinFrost);
|
||||
}
|
||||
|
||||
coinEntities.addAll(_coins.map((e) => CoinEntity(e)));
|
||||
|
||||
if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) {
|
||||
if (!ref.read(prefsChangeNotifierProvider).frostEnabled) {
|
||||
_coinsTestnet.remove(Coin.bitcoinFrostTestNet);
|
||||
}
|
||||
|
||||
coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e)));
|
||||
}
|
||||
|
||||
|
|
|
@ -45,48 +45,55 @@ class CreateOrRestoreWalletView extends StatelessWidget {
|
|||
leading: AppBarBackButton(),
|
||||
trailing: ExitToMyStackButton(),
|
||||
),
|
||||
body: SizedBox(
|
||||
width: 480,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(
|
||||
flex: 10,
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
// Center the content horizontally
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/*const Spacer(
|
||||
flex: 10,
|
||||
),*/
|
||||
CreateRestoreWalletTitle(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 324,
|
||||
child: CreateRestoreWalletSubTitle(
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CoinImage(
|
||||
coin: entity.coin,
|
||||
width: isDesktop
|
||||
? 324
|
||||
: MediaQuery.of(context).size.width / 1.6,
|
||||
height: isDesktop
|
||||
? null
|
||||
: MediaQuery.of(context).size.width / 1.6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CreateWalletButtonGroup(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
/*const Spacer(
|
||||
flex: 15,
|
||||
),*/
|
||||
],
|
||||
),
|
||||
CreateRestoreWalletTitle(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: 324,
|
||||
child: CreateRestoreWalletSubTitle(
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CoinImage(
|
||||
coin: entity.coin,
|
||||
width:
|
||||
isDesktop ? 324 : MediaQuery.of(context).size.width / 1.6,
|
||||
height:
|
||||
isDesktop ? null : MediaQuery.of(context).size.width / 1.6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
CreateWalletButtonGroup(
|
||||
coin: entity.coin,
|
||||
isDesktop: isDesktop,
|
||||
),
|
||||
const Spacer(
|
||||
flex: 15,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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:flutter/material.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class FrostStepExplanationDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String body;
|
||||
const FrostStepExplanationDialog({super.key, required this.title, required this.body});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Text(
|
||||
body,
|
||||
style: STextStyles.baseXS(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Close",
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.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/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||
|
||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||
|
||||
class ConfirmNewFrostMSWalletCreationView extends ConsumerStatefulWidget {
|
||||
const ConfirmNewFrostMSWalletCreationView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/confirmNewFrostMSWalletCreationView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<ConfirmNewFrostMSWalletCreationView> createState() =>
|
||||
_ConfirmNewFrostMSWalletCreationViewState();
|
||||
}
|
||||
|
||||
class _ConfirmNewFrostMSWalletCreationViewState
|
||||
extends ConsumerState<ConfirmNewFrostMSWalletCreationView> {
|
||||
late final String seed, recoveryString, serializedKeys, multisigConfig;
|
||||
late final Uint8List multisigId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
seed = ref.read(pFrostStartKeyGenData.state).state!.seed;
|
||||
serializedKeys =
|
||||
ref.read(pFrostCompletedKeyGenData.state).state!.serializedKeys;
|
||||
recoveryString =
|
||||
ref.read(pFrostCompletedKeyGenData.state).state!.recoveryString;
|
||||
multisigId = ref.read(pFrostCompletedKeyGenData.state).state!.multisigId;
|
||||
multisigConfig = ref.read(pFrostMultisigConfig.state).state!;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName:
|
||||
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: HomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Finalize FROST multisig wallet",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Ensure your multisig ID matches that of each other participant",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "ID",
|
||||
detail: multisigId.toString(),
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: multisigId.toString(),
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: multisigId.toString(),
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
const _Div(),
|
||||
Text(
|
||||
"Back up your keys and config",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "Multisig Config",
|
||||
detail: multisigConfig,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: multisigConfig,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: multisigConfig,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "Keys",
|
||||
detail: serializedKeys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: serializedKeys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: serializedKeys,
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Confirm",
|
||||
onPressed: () async {
|
||||
bool progressPopped = false;
|
||||
try {
|
||||
unawaited(
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
useSafeArea: true,
|
||||
builder: (ctx) {
|
||||
return const Center(
|
||||
child: LoadingIndicator(
|
||||
width: 50,
|
||||
height: 50,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final info = WalletInfo.createNew(
|
||||
coin: widget.coin,
|
||||
name: widget.walletName,
|
||||
);
|
||||
|
||||
final wallet = await Wallet.create(
|
||||
walletInfo: info,
|
||||
mainDB: ref.read(mainDBProvider),
|
||||
secureStorageInterface: ref.read(secureStoreProvider),
|
||||
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
);
|
||||
|
||||
await (wallet as BitcoinFrostWallet).initializeNewFrost(
|
||||
mnemonic: seed,
|
||||
multisigConfig: multisigConfig,
|
||||
recoveryString: recoveryString,
|
||||
serializedKeys: serializedKeys,
|
||||
multisigId: multisigId,
|
||||
myName: ref.read(pFrostMyName.state).state!,
|
||||
participants: Frost.getParticipants(
|
||||
multisigConfig:
|
||||
ref.read(pFrostMultisigConfig.state).state!,
|
||||
),
|
||||
threshold: Frost.getThreshold(
|
||||
multisigConfig:
|
||||
ref.read(pFrostMultisigConfig.state).state!,
|
||||
),
|
||||
);
|
||||
|
||||
await info.setMnemonicVerified(
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
|
||||
ref.read(pWallets).addWallet(wallet);
|
||||
|
||||
// pop progress dialog
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
progressPopped = true;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (Util.isDesktop) {
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(
|
||||
DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
HomeView.routeName,
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(pFrostMultisigConfig.state).state = null;
|
||||
ref.read(pFrostStartKeyGenData.state).state = null;
|
||||
ref.read(pFrostSecretSharesData.state).state = null;
|
||||
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.success,
|
||||
message: "Your wallet is set up.",
|
||||
iconAsset: Assets.svg.check,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
// pop progress dialog
|
||||
if (mounted && !progressPopped) {
|
||||
Navigator.pop(context);
|
||||
progressPopped = true;
|
||||
}
|
||||
// TODO: handle gracefully
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,288 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/stack_dialog.dart';
|
||||
|
||||
class CreateNewFrostMsWalletView extends ConsumerStatefulWidget {
|
||||
const CreateNewFrostMsWalletView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/createNewFrostMsWalletView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<CreateNewFrostMsWalletView> createState() =>
|
||||
_NewFrostMsWalletViewState();
|
||||
}
|
||||
|
||||
class _NewFrostMsWalletViewState
|
||||
extends ConsumerState<CreateNewFrostMsWalletView> {
|
||||
final _thresholdController = TextEditingController();
|
||||
final _participantsController = TextEditingController();
|
||||
|
||||
final List<TextEditingController> controllers = [];
|
||||
|
||||
int _participantsCount = 0;
|
||||
|
||||
String _validateInputData() {
|
||||
final threshold = int.tryParse(_thresholdController.text);
|
||||
if (threshold == null) {
|
||||
return "Choose a threshold";
|
||||
}
|
||||
|
||||
final partsCount = int.tryParse(_participantsController.text);
|
||||
if (partsCount == null) {
|
||||
return "Choose total number of participants";
|
||||
}
|
||||
|
||||
if (threshold > partsCount) {
|
||||
return "Threshold cannot be greater than the number of participants";
|
||||
}
|
||||
|
||||
if (partsCount < 2) {
|
||||
return "At least two participants required";
|
||||
}
|
||||
|
||||
if (controllers.length != partsCount) {
|
||||
return "Participants count error";
|
||||
}
|
||||
|
||||
final hasEmptyParticipants = controllers
|
||||
.map((e) => e.text.isEmpty)
|
||||
.reduce((value, element) => value |= element);
|
||||
if (hasEmptyParticipants) {
|
||||
return "Participants must not be empty";
|
||||
}
|
||||
|
||||
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
|
||||
return "Duplicate participant name found";
|
||||
}
|
||||
|
||||
return "valid";
|
||||
}
|
||||
|
||||
void _participantsCountChanged(String newValue) {
|
||||
final count = int.tryParse(newValue);
|
||||
if (count != null) {
|
||||
if (count > _participantsCount) {
|
||||
for (int i = _participantsCount; i < count; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
}
|
||||
|
||||
_participantsCount = count;
|
||||
setState(() {});
|
||||
} else if (count < _participantsCount) {
|
||||
for (int i = _participantsCount; i > count; i--) {
|
||||
final last = controllers.removeLast();
|
||||
last.dispose();
|
||||
}
|
||||
|
||||
_participantsCount = count;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_thresholdController.dispose();
|
||||
_participantsController.dispose();
|
||||
for (final e in controllers) {
|
||||
e.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"New FROST multisig config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Threshold",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
controller: _thresholdController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Number of participants",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
controller: _participantsController,
|
||||
onChanged: _participantsCountChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
Text(
|
||||
"My name",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
TextField(
|
||||
controller: controllers.first,
|
||||
),
|
||||
if (controllers.length > 1)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (controllers.length > 1)
|
||||
Text(
|
||||
"Remaining participants",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
if (controllers.length > 1)
|
||||
Column(
|
||||
children: [
|
||||
for (int i = 1; i < controllers.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controllers[i],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Generate",
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
final validationMessage = _validateInputData();
|
||||
|
||||
if (validationMessage != "valid") {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: validationMessage,
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final config = Frost.createMultisigConfig(
|
||||
name: controllers.first.text,
|
||||
threshold: int.parse(_thresholdController.text),
|
||||
participants: controllers.map((e) => e.text).toList(),
|
||||
);
|
||||
|
||||
ref.read(pFrostMyName.notifier).state = controllers.first.text;
|
||||
ref.read(pFrostMultisigConfig.notifier).state = config;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
ShareNewMultisigConfigView.routeName,
|
||||
arguments: (
|
||||
walletName: widget.walletName,
|
||||
coin: widget.coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class FrostShareCommitmentsView extends ConsumerStatefulWidget {
|
||||
const FrostShareCommitmentsView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostShareCommitmentsView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostShareCommitmentsView> createState() =>
|
||||
_FrostShareCommitmentsViewState();
|
||||
}
|
||||
|
||||
class _FrostShareCommitmentsViewState
|
||||
extends ConsumerState<FrostShareCommitmentsView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<String> participants;
|
||||
late final String myCommitment;
|
||||
late final int myIndex;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
participants = Frost.getParticipants(
|
||||
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||
);
|
||||
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
|
||||
myCommitment = ref.read(pFrostStartKeyGenData.state).state!.commitments;
|
||||
|
||||
// temporarily remove my name
|
||||
participants.removeAt(myIndex);
|
||||
|
||||
for (int i = 0; i < participants.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName:
|
||||
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: HomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Commitments",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myCommitment,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My name",
|
||||
detail: ref.watch(pFrostMyName.state).state!,
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My commitment",
|
||||
detail: myCommitment,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myCommitment,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myCommitment,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < participants.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostCommitmentsTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${participants[i]}'s commitment",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Commitment Field Input.",
|
||||
key: Key(
|
||||
"frostCommitmentsClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Commitment Field Input.",
|
||||
key: Key(
|
||||
"frostCommitmentsPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostCommitmentsScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Generate shares",
|
||||
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
onPressed: () async {
|
||||
// check for empty commitments
|
||||
if (controllers
|
||||
.map((e) => e.text.isEmpty)
|
||||
.reduce((value, element) => value |= element)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Missing commitments",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// collect commitment strings and insert my own at the correct index
|
||||
final commitments = controllers.map((e) => e.text).toList();
|
||||
commitments.insert(myIndex, myCommitment);
|
||||
|
||||
try {
|
||||
ref.read(pFrostSecretSharesData.notifier).state =
|
||||
Frost.generateSecretShares(
|
||||
multisigConfigWithNamePtr: ref
|
||||
.read(pFrostStartKeyGenData.state)
|
||||
.state!
|
||||
.multisigConfigWithNamePtr,
|
||||
mySeed: ref.read(pFrostStartKeyGenData.state).state!.seed,
|
||||
secretShareMachineWrapperPtr: ref
|
||||
.read(pFrostStartKeyGenData.state)
|
||||
.state!
|
||||
.secretShareMachineWrapperPtr,
|
||||
commitments: commitments,
|
||||
);
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostShareSharesView.routeName,
|
||||
arguments: (
|
||||
walletName: widget.walletName,
|
||||
coin: widget.coin,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Failed to generate shares",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class FrostShareSharesView extends ConsumerStatefulWidget {
|
||||
const FrostShareSharesView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostShareSharesView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostShareSharesView> createState() =>
|
||||
_FrostShareSharesViewState();
|
||||
}
|
||||
|
||||
class _FrostShareSharesViewState extends ConsumerState<FrostShareSharesView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<String> participants;
|
||||
late final String myShare;
|
||||
late final int myIndex;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
participants = Frost.getParticipants(
|
||||
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||
);
|
||||
myIndex = participants.indexOf(ref.read(pFrostMyName.state).state!);
|
||||
myShare = ref.read(pFrostSecretSharesData.state).state!.share;
|
||||
|
||||
// temporarily remove my name. Added back later
|
||||
participants.removeAt(myIndex);
|
||||
|
||||
for (int i = 0; i < participants.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName:
|
||||
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.walletCreation,
|
||||
popUntilOnYesRouteName: HomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Generate shares",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myShare,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My name",
|
||||
detail: ref.watch(pFrostMyName.state).state!,
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My share",
|
||||
detail: myShare,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myShare,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myShare,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
for (int i = 0; i < participants.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frSharesTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${participants[i]}'s share",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Share Field Input.",
|
||||
key: Key("frSharesClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Share Field Input.",
|
||||
key: Key("frSharesPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: Key("frSharesScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Generate",
|
||||
onPressed: () async {
|
||||
// check for empty commitments
|
||||
if (controllers
|
||||
.map((e) => e.text.isEmpty)
|
||||
.reduce((value, element) => value |= element)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Missing shares",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// collect commitment strings and insert my own at the correct index
|
||||
final shares = controllers.map((e) => e.text).toList();
|
||||
shares.insert(myIndex, myShare);
|
||||
|
||||
try {
|
||||
ref.read(pFrostCompletedKeyGenData.notifier).state =
|
||||
Frost.completeKeyGeneration(
|
||||
multisigConfigWithNamePtr: ref
|
||||
.read(pFrostStartKeyGenData.state)
|
||||
.state!
|
||||
.multisigConfigWithNamePtr,
|
||||
secretSharesResPtr: ref
|
||||
.read(pFrostSecretSharesData.state)
|
||||
.state!
|
||||
.secretSharesResPtr,
|
||||
shares: shares,
|
||||
);
|
||||
await Navigator.of(context).pushNamed(
|
||||
ConfirmNewFrostMSWalletCreationView.routeName,
|
||||
arguments: (
|
||||
walletName: widget.walletName,
|
||||
coin: widget.coin,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Failed to complete key generation",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,390 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/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/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class ImportNewFrostMsWalletView extends ConsumerStatefulWidget {
|
||||
const ImportNewFrostMsWalletView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/importNewFrostMsWalletView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<ImportNewFrostMsWalletView> createState() =>
|
||||
_ImportNewFrostMsWalletViewState();
|
||||
}
|
||||
|
||||
class _ImportNewFrostMsWalletViewState
|
||||
extends ConsumerState<ImportNewFrostMsWalletView> {
|
||||
late final TextEditingController myNameFieldController, configFieldController;
|
||||
late final FocusNode myNameFocusNode, configFocusNode;
|
||||
|
||||
bool _nameEmpty = true, _configEmpty = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
myNameFieldController = TextEditingController();
|
||||
configFieldController = TextEditingController();
|
||||
myNameFocusNode = FocusNode();
|
||||
configFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
myNameFieldController.dispose();
|
||||
configFieldController.dispose();
|
||||
myNameFocusNode.dispose();
|
||||
configFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Import FROST multisig config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frMyNameTextFieldKey"),
|
||||
controller: myNameFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_nameEmpty = myNameFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: myNameFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"My name",
|
||||
myNameFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _nameEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_nameEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frMyNameClearButtonKey"),
|
||||
onTap: () {
|
||||
myNameFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_nameEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Name Field.",
|
||||
key: const Key("frMyNamePasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
myNameFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_nameEmpty =
|
||||
myNameFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _nameEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frConfigTextFieldKey"),
|
||||
controller: configFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: configFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter config",
|
||||
configFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _configEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_configEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frConfigClearButtonKey"),
|
||||
onTap: () {
|
||||
configFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_configEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||
key: const Key("frConfigPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
configFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _configEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_configEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start key generation",
|
||||
enabled: !_nameEmpty && !_configEmpty,
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
final config = configFieldController.text;
|
||||
|
||||
if (!Frost.validateEncodedMultisigConfig(
|
||||
encodedConfig: config)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Invalid config",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!Frost.getParticipants(multisigConfig: config)
|
||||
.contains(myNameFieldController.text)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "My name not found in config participants",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ref.read(pFrostMyName.state).state = myNameFieldController.text;
|
||||
ref.read(pFrostMultisigConfig.notifier).state = config;
|
||||
|
||||
ref.read(pFrostStartKeyGenData.state).state =
|
||||
Frost.startKeyGeneration(
|
||||
multisigConfig: ref.read(pFrostMultisigConfig.state).state!,
|
||||
myName: ref.read(pFrostMyName.state).state!,
|
||||
);
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostShareCommitmentsView.routeName,
|
||||
arguments: (
|
||||
walletName: widget.walletName,
|
||||
coin: widget.coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/custom_buttons/simple_copy_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/detail_item.dart';
|
||||
|
||||
class ShareNewMultisigConfigView extends ConsumerStatefulWidget {
|
||||
const ShareNewMultisigConfigView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/shareNewMultisigConfigView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<ShareNewMultisigConfigView> createState() =>
|
||||
_ShareNewMultisigConfigViewState();
|
||||
}
|
||||
|
||||
class _ShareNewMultisigConfigViewState
|
||||
extends ConsumerState<ShareNewMultisigConfigView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Multisig config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data:
|
||||
ref.watch(pFrostMultisigConfig.state).state ?? "Error",
|
||||
size: 220,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Encoded config",
|
||||
detail: ref.watch(pFrostMultisigConfig.state).state ?? "Error",
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: ref.watch(pFrostMultisigConfig.state).state ??
|
||||
"Error",
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: ref.watch(pFrostMultisigConfig.state).state ??
|
||||
"Error",
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 64 : 16,
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start key generation",
|
||||
onPressed: () async {
|
||||
ref.read(pFrostStartKeyGenData.notifier).state =
|
||||
Frost.startKeyGeneration(
|
||||
multisigConfig: ref.watch(pFrostMultisigConfig.state).state!,
|
||||
myName: ref.read(pFrostMyName.state).state!,
|
||||
);
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostShareCommitmentsView.routeName,
|
||||
arguments: (
|
||||
walletName: widget.walletName,
|
||||
coin: widget.coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,485 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:frostdart/frostdart.dart' as frost;
|
||||
import 'package:stackwallet/notifications/show_flush_bar.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';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.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/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet.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/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class RestoreFrostMsWalletView extends ConsumerStatefulWidget {
|
||||
const RestoreFrostMsWalletView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/restoreFrostMsWalletView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<RestoreFrostMsWalletView> createState() =>
|
||||
_RestoreFrostMsWalletViewState();
|
||||
}
|
||||
|
||||
class _RestoreFrostMsWalletViewState
|
||||
extends ConsumerState<RestoreFrostMsWalletView> {
|
||||
late final TextEditingController keysFieldController, configFieldController;
|
||||
late final FocusNode keysFocusNode, configFocusNode;
|
||||
|
||||
bool _keysEmpty = true, _configEmpty = true;
|
||||
|
||||
bool _restoreButtonLock = false;
|
||||
|
||||
Future<Wallet> _createWalletAndRecover() async {
|
||||
final keys = keysFieldController.text;
|
||||
final config = configFieldController.text;
|
||||
|
||||
final myNameIndex = frost.getParticipantIndexFromKeys(serializedKeys: keys);
|
||||
final participants = Frost.getParticipants(multisigConfig: config);
|
||||
final myName = participants[myNameIndex];
|
||||
|
||||
final info = WalletInfo.createNew(
|
||||
coin: widget.coin,
|
||||
name: widget.walletName,
|
||||
);
|
||||
|
||||
final wallet = await Wallet.create(
|
||||
walletInfo: info,
|
||||
mainDB: ref.read(mainDBProvider),
|
||||
secureStorageInterface: ref.read(secureStoreProvider),
|
||||
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
);
|
||||
|
||||
final frostInfo = FrostWalletInfo(
|
||||
walletId: info.walletId,
|
||||
knownSalts: [],
|
||||
participants: participants,
|
||||
myName: myName,
|
||||
threshold: frost.multisigThreshold(
|
||||
multisigConfig: config,
|
||||
),
|
||||
);
|
||||
|
||||
await ref.read(mainDBProvider).isar.writeTxn(() async {
|
||||
await ref.read(mainDBProvider).isar.frostWalletInfo.put(frostInfo);
|
||||
});
|
||||
|
||||
await (wallet as BitcoinFrostWallet).recover(
|
||||
serializedKeys: keys,
|
||||
multisigConfig: config,
|
||||
isRescan: false,
|
||||
);
|
||||
|
||||
await info.setMnemonicVerified(
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
Future<void> _restore() async {
|
||||
if (_restoreButtonLock) {
|
||||
return;
|
||||
}
|
||||
_restoreButtonLock = true;
|
||||
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
Exception? ex;
|
||||
final wallet = await showLoading(
|
||||
whileFuture: _createWalletAndRecover(),
|
||||
context: context,
|
||||
message: "Restoring wallet...",
|
||||
isDesktop: Util.isDesktop,
|
||||
onException: (e) {
|
||||
ex = e;
|
||||
},
|
||||
);
|
||||
|
||||
if (ex != null) {
|
||||
throw ex!;
|
||||
}
|
||||
|
||||
ref.read(pWallets).addWallet(wallet!);
|
||||
|
||||
if (mounted) {
|
||||
if (Util.isDesktop) {
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(
|
||||
DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
HomeView.routeName,
|
||||
(route) => false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.success,
|
||||
message: "Your wallet is set up.",
|
||||
iconAsset: Assets.svg.check,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Failed to restore",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_restoreButtonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
keysFieldController = TextEditingController();
|
||||
configFieldController = TextEditingController();
|
||||
keysFocusNode = FocusNode();
|
||||
configFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
keysFieldController.dispose();
|
||||
configFieldController.dispose();
|
||||
keysFocusNode.dispose();
|
||||
configFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
)
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Restore FROST multisig wallet",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frMyNameTextFieldKey"),
|
||||
controller: keysFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_keysEmpty = keysFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: keysFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Keys",
|
||||
keysFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _keysEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_keysEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Keys Field.",
|
||||
key: const Key("frMyNameClearButtonKey"),
|
||||
onTap: () {
|
||||
keysFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_keysEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Keys Field.",
|
||||
key: const Key("frKeysPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
keysFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_keysEmpty =
|
||||
keysFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _keysEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frConfigTextFieldKey"),
|
||||
controller: configFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: configFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter config",
|
||||
configFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _configEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_configEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frConfigClearButtonKey"),
|
||||
onTap: () {
|
||||
configFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_configEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||
key: const Key("frConfigPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
configFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _configEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_configEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Restore",
|
||||
enabled: !_keysEmpty && !_configEmpty,
|
||||
onPressed: _restore,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,9 +14,13 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.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/frost_ms/new/create_new_frost_ms_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_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_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/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/ui/verify_recovery_phrase/mnemonic_word_count_state_provider.dart';
|
||||
|
@ -32,6 +36,8 @@ import 'package:stackwallet/widgets/background.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/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/dice_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
@ -77,6 +83,52 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
|||
return name;
|
||||
}
|
||||
|
||||
Future<void> _nextPressed() async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
if (mounted) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
isDesktop = Util.isDesktop;
|
||||
|
@ -330,78 +382,104 @@ class _NameYourWalletViewState extends ConsumerState<NameYourWalletView> {
|
|||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: isDesktop ? 480 : 0,
|
||||
minHeight: isDesktop ? 70 : 0,
|
||||
if (widget.coin.isFrost)
|
||||
if (widget.addWalletType == AddWalletType.Restore)
|
||||
PrimaryButton(
|
||||
label: "Next",
|
||||
enabled: _nextEnabled,
|
||||
onPressed: () async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
RestoreFrostMsWalletView.routeName,
|
||||
arguments: (
|
||||
walletName: name,
|
||||
coin: coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (widget.coin.isFrost && widget.addWalletType == AddWalletType.New)
|
||||
Column(
|
||||
children: [
|
||||
PrimaryButton(
|
||||
label: "Create config",
|
||||
enabled: _nextEnabled,
|
||||
onPressed: () async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
CreateNewFrostMsWalletView.routeName,
|
||||
arguments: (
|
||||
walletName: name,
|
||||
coin: coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Import multisig config",
|
||||
enabled: _nextEnabled,
|
||||
onPressed: () async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
ImportNewFrostMsWalletView.routeName,
|
||||
arguments: (
|
||||
walletName: name,
|
||||
coin: coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
SecondaryButton(
|
||||
label: "Import resharer config",
|
||||
enabled: _nextEnabled,
|
||||
onPressed: () async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
NewImportResharerConfigView.routeName,
|
||||
arguments: (
|
||||
walletName: name,
|
||||
coin: coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: _nextEnabled
|
||||
? () async {
|
||||
final name = textEditingController.text;
|
||||
|
||||
if (mounted) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
style: _nextEnabled
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context)
|
||||
: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryDisabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Next",
|
||||
style: isDesktop
|
||||
? _nextEnabled
|
||||
? STextStyles.desktopButtonEnabled(context)
|
||||
: STextStyles.desktopButtonDisabled(context)
|
||||
: STextStyles.button(context),
|
||||
if (!widget.coin.isFrost)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minWidth: isDesktop ? 480 : 0,
|
||||
minHeight: isDesktop ? 70 : 0,
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: _nextEnabled ? _nextPressed : null,
|
||||
style: _nextEnabled
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context)
|
||||
: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryDisabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Next",
|
||||
style: isDesktop
|
||||
? _nextEnabled
|
||||
? STextStyles.desktopButtonEnabled(context)
|
||||
: STextStyles.desktopButtonDisabled(context)
|
||||
: STextStyles.button(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
const Spacer(
|
||||
flex: 15,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -56,6 +56,7 @@ import 'package:stackwallet/wallets/wallet/impl/monero_wallet.dart';
|
|||
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet.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';
|
||||
|
@ -250,6 +251,12 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
),
|
||||
},
|
||||
);
|
||||
} else if (widget.coin == Coin.firo) {
|
||||
otherDataJsonString = jsonEncode(
|
||||
{
|
||||
WalletInfoKeys.lelantusCoinIsarRescanRequired: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: do actual check to make sure it is a valid mnemonic for monero
|
||||
|
@ -717,352 +724,376 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
],
|
||||
),
|
||||
body: Container(
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (isDesktop)
|
||||
color: Theme.of(context).extension<StackColors>()!.background,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
/*if (isDesktop)
|
||||
const Spacer(
|
||||
flex: 10,
|
||||
),
|
||||
if (!isDesktop)
|
||||
Text(
|
||||
widget.walletName,
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 0 : 4,
|
||||
),
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopH2(context)
|
||||
: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 16 : 8,
|
||||
),
|
||||
Text(
|
||||
"Enter your $_seedWordCount-word recovery phrase.",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopSubtitleH2(context)
|
||||
: STextStyles.subtitle(context),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 16 : 10,
|
||||
),
|
||||
if (isDesktop)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: pasteMnemonic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.clipboard,
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonTextSecondary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
"Paste",
|
||||
style: STextStyles
|
||||
.desktopButtonSmallSecondaryEnabled(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),*/
|
||||
if (!isDesktop)
|
||||
Text(
|
||||
widget.walletName,
|
||||
style: STextStyles.itemSubtitle(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
if (isDesktop)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1008,
|
||||
SizedBox(
|
||||
height: isDesktop ? 0 : 4,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
const cols = 4;
|
||||
final int rows = _seedWordCount ~/ cols;
|
||||
final int remainder = _seedWordCount % cols;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: TableView(
|
||||
shrinkWrap: true,
|
||||
rowSpacing: 20,
|
||||
rows: [
|
||||
for (int i = 0; i < rows; i++)
|
||||
TableViewRow(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
spacing: 16,
|
||||
cells: [
|
||||
for (int j = 1; j <= cols; j++)
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autocorrect: !isDesktop,
|
||||
enableSuggestions: !isDesktop,
|
||||
textCapitalization:
|
||||
TextCapitalization.none,
|
||||
key: Key(
|
||||
"restoreMnemonicFormField_$i"),
|
||||
decoration:
|
||||
_getInputDecorationFor(
|
||||
_inputStatuses[
|
||||
i * 4 + j - 1],
|
||||
"${i * 4 + j}"),
|
||||
autovalidateMode:
|
||||
AutovalidateMode
|
||||
.onUserInteraction,
|
||||
selectionControls:
|
||||
i * 4 + j - 1 == 1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
// focusNode:
|
||||
// _focusNodes[i * 4 + j - 1],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
||||
if (value.isEmpty) {
|
||||
formInputStatus =
|
||||
FormInputStatus.empty;
|
||||
} else if (_isValidMnemonicWord(
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase())) {
|
||||
formInputStatus =
|
||||
FormInputStatus.valid;
|
||||
} else {
|
||||
formInputStatus =
|
||||
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();
|
||||
// }
|
||||
// }
|
||||
setState(() {
|
||||
_inputStatuses[i * 4 +
|
||||
j -
|
||||
1] = formInputStatus;
|
||||
});
|
||||
},
|
||||
controller:
|
||||
_controllers[i * 4 + j - 1],
|
||||
style:
|
||||
STextStyles.field(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textRestore,
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
),
|
||||
),
|
||||
if (_inputStatuses[
|
||||
i * 4 + j - 1] ==
|
||||
FormInputStatus.invalid)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
"Please check spelling",
|
||||
textAlign: TextAlign.left,
|
||||
style: STextStyles.label(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textError,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
expandingChild: null,
|
||||
),
|
||||
if (remainder > 0)
|
||||
TableViewRow(
|
||||
spacing: 16,
|
||||
cells: [
|
||||
for (int i = rows * cols;
|
||||
i < _seedWordCount;
|
||||
i++) ...[
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autocorrect: !isDesktop,
|
||||
enableSuggestions: !isDesktop,
|
||||
textCapitalization:
|
||||
TextCapitalization.none,
|
||||
key: Key(
|
||||
"restoreMnemonicFormField_$i"),
|
||||
decoration:
|
||||
_getInputDecorationFor(
|
||||
_inputStatuses[i],
|
||||
"${i + 1}"),
|
||||
autovalidateMode:
|
||||
AutovalidateMode
|
||||
.onUserInteraction,
|
||||
selectionControls: i == 1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
// focusNode: _focusNodes[i],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
||||
if (value.isEmpty) {
|
||||
formInputStatus =
|
||||
FormInputStatus.empty;
|
||||
} else if (_isValidMnemonicWord(
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase())) {
|
||||
formInputStatus =
|
||||
FormInputStatus.valid;
|
||||
} else {
|
||||
formInputStatus =
|
||||
FormInputStatus.invalid;
|
||||
}
|
||||
|
||||
// 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();
|
||||
// }
|
||||
// }
|
||||
},
|
||||
controller: _controllers[i],
|
||||
style:
|
||||
STextStyles.field(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.overlay,
|
||||
fontSize: isDesktop ? 16 : 14,
|
||||
),
|
||||
),
|
||||
if (_inputStatuses[i] ==
|
||||
FormInputStatus.invalid)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(
|
||||
left: 12.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
"Please check spelling",
|
||||
textAlign: TextAlign.left,
|
||||
style: STextStyles.label(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textError,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
for (int i = remainder;
|
||||
i < cols;
|
||||
i++) ...[
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Container(),
|
||||
),
|
||||
],
|
||||
],
|
||||
expandingChild: null,
|
||||
),
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopH2(context)
|
||||
: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 16 : 8,
|
||||
),
|
||||
Text(
|
||||
"Enter your $_seedWordCount-word recovery phrase.",
|
||||
style: isDesktop
|
||||
? STextStyles.desktopSubtitleH2(context)
|
||||
: STextStyles.subtitle(context),
|
||||
),
|
||||
SizedBox(
|
||||
height: isDesktop ? 16 : 10,
|
||||
),
|
||||
if (isDesktop)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: pasteMnemonic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.clipboard,
|
||||
width: 22,
|
||||
height: 22,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonTextSecondary,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(
|
||||
"Paste",
|
||||
style: STextStyles
|
||||
.desktopButtonSmallSecondaryEnabled(
|
||||
context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Restore wallet",
|
||||
width: 480,
|
||||
onPressed: requestRestore,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isDesktop)
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
if (isDesktop)
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 1008,
|
||||
),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
const cols = 4;
|
||||
final int rows = _seedWordCount ~/ cols;
|
||||
final int remainder = _seedWordCount % cols;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: TableView(
|
||||
shrinkWrap: true,
|
||||
rowSpacing: 20,
|
||||
rows: [
|
||||
for (int i = 0; i < rows; i++)
|
||||
TableViewRow(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
spacing: 16,
|
||||
cells: [
|
||||
for (int j = 1; j <= cols; j++)
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autocorrect: !isDesktop,
|
||||
enableSuggestions:
|
||||
!isDesktop,
|
||||
textCapitalization:
|
||||
TextCapitalization.none,
|
||||
key: Key(
|
||||
"restoreMnemonicFormField_$i"),
|
||||
decoration:
|
||||
_getInputDecorationFor(
|
||||
_inputStatuses[
|
||||
i * 4 + j - 1],
|
||||
"${i * 4 + j}"),
|
||||
autovalidateMode:
|
||||
AutovalidateMode
|
||||
.onUserInteraction,
|
||||
selectionControls: i * 4 +
|
||||
j -
|
||||
1 ==
|
||||
1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
// focusNode:
|
||||
// _focusNodes[i * 4 + j - 1],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
||||
if (value.isEmpty) {
|
||||
formInputStatus =
|
||||
FormInputStatus
|
||||
.empty;
|
||||
} else if (_isValidMnemonicWord(
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase())) {
|
||||
formInputStatus =
|
||||
FormInputStatus
|
||||
.valid;
|
||||
} else {
|
||||
formInputStatus =
|
||||
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();
|
||||
// }
|
||||
// }
|
||||
setState(() {
|
||||
_inputStatuses[
|
||||
i * 4 + j - 1] =
|
||||
formInputStatus;
|
||||
});
|
||||
},
|
||||
controller: _controllers[
|
||||
i * 4 + j - 1],
|
||||
style: STextStyles.field(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textRestore,
|
||||
fontSize:
|
||||
isDesktop ? 16 : 14,
|
||||
),
|
||||
),
|
||||
if (_inputStatuses[
|
||||
i * 4 + j - 1] ==
|
||||
FormInputStatus.invalid)
|
||||
Align(
|
||||
alignment:
|
||||
Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
left: 12.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
"Please check spelling",
|
||||
textAlign:
|
||||
TextAlign.left,
|
||||
style:
|
||||
STextStyles.label(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textError,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
expandingChild: null,
|
||||
),
|
||||
if (remainder > 0)
|
||||
TableViewRow(
|
||||
spacing: 16,
|
||||
cells: [
|
||||
for (int i = rows * cols;
|
||||
i < _seedWordCount;
|
||||
i++) ...[
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
autocorrect: !isDesktop,
|
||||
enableSuggestions:
|
||||
!isDesktop,
|
||||
textCapitalization:
|
||||
TextCapitalization.none,
|
||||
key: Key(
|
||||
"restoreMnemonicFormField_$i"),
|
||||
decoration:
|
||||
_getInputDecorationFor(
|
||||
_inputStatuses[i],
|
||||
"${i + 1}"),
|
||||
autovalidateMode:
|
||||
AutovalidateMode
|
||||
.onUserInteraction,
|
||||
selectionControls: i == 1
|
||||
? textSelectionControls
|
||||
: null,
|
||||
// focusNode: _focusNodes[i],
|
||||
onChanged: (value) {
|
||||
final FormInputStatus
|
||||
formInputStatus;
|
||||
|
||||
if (value.isEmpty) {
|
||||
formInputStatus =
|
||||
FormInputStatus
|
||||
.empty;
|
||||
} else if (_isValidMnemonicWord(
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase())) {
|
||||
formInputStatus =
|
||||
FormInputStatus
|
||||
.valid;
|
||||
} else {
|
||||
formInputStatus =
|
||||
FormInputStatus
|
||||
.invalid;
|
||||
}
|
||||
|
||||
// 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();
|
||||
// }
|
||||
// }
|
||||
},
|
||||
controller: _controllers[i],
|
||||
style: STextStyles.field(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.overlay,
|
||||
fontSize:
|
||||
isDesktop ? 16 : 14,
|
||||
),
|
||||
),
|
||||
if (_inputStatuses[i] ==
|
||||
FormInputStatus.invalid)
|
||||
Align(
|
||||
alignment:
|
||||
Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets
|
||||
.only(
|
||||
left: 12.0,
|
||||
bottom: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
"Please check spelling",
|
||||
textAlign:
|
||||
TextAlign.left,
|
||||
style:
|
||||
STextStyles.label(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(
|
||||
context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textError,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
for (int i = remainder;
|
||||
i < cols;
|
||||
i++) ...[
|
||||
TableViewCell(
|
||||
flex: 1,
|
||||
child: Container(),
|
||||
),
|
||||
],
|
||||
],
|
||||
expandingChild: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Restore wallet",
|
||||
width: 480,
|
||||
onPressed: requestRestore,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
/*if (isDesktop)
|
||||
const Spacer(
|
||||
flex: 15,
|
||||
),
|
||||
if (!isDesktop)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Padding(
|
||||
),*/
|
||||
if (!isDesktop)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
|
@ -1162,12 +1193,11 @@ class _RestoreWalletViewState extends ConsumerState<RestoreWalletView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
@ -65,13 +66,21 @@ class _RestoreFailedDialogState extends ConsumerState<RestoreFailedDialog> {
|
|||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
onPressed: () async {
|
||||
await ref.read(pWallets).deleteWallet(
|
||||
ref.read(pWalletInfo(walletId)),
|
||||
ref.read(secureStoreProvider),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
await ref.read(pWallets).deleteWallet(
|
||||
ref.read(pWalletInfo(walletId)),
|
||||
ref.read(secureStoreProvider),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Error while getting wallet info in restore failed dialog\n"
|
||||
"Error: $e\nStack trace: $s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
@ -51,7 +51,7 @@ class RestoreSucceededDialog extends StatelessWidget {
|
|||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"You can use your wallet now.",
|
||||
"You may access your wallet now.",
|
||||
style: STextStyles.desktopTextMedium(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark3,
|
||||
),
|
||||
|
@ -80,7 +80,7 @@ class RestoreSucceededDialog extends StatelessWidget {
|
|||
} else {
|
||||
return StackDialog(
|
||||
title: "Wallet restored",
|
||||
message: "You can use your wallet now.",
|
||||
message: "You may access your wallet now.",
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.checkCircle,
|
||||
width: 24,
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
@ -234,9 +236,13 @@ class _EditContactAddressViewState
|
|||
e.coin == addressEntry.coin,
|
||||
);
|
||||
|
||||
_addresses.remove(entry);
|
||||
//Deleting an entry directly from _addresses gives error
|
||||
// "Cannot remove from a fixed-length list", so we remove the
|
||||
// entry from a copy
|
||||
var tempAddresses = List<ContactAddressEntry>.from(_addresses);
|
||||
tempAddresses.remove(entry);
|
||||
ContactEntry editedContact =
|
||||
contact.copyWith(addresses: _addresses);
|
||||
contact.copyWith(addresses: tempAddresses);
|
||||
if (await ref
|
||||
.read(addressBookServiceProvider)
|
||||
.editContact(editedContact)) {
|
||||
|
|
|
@ -138,7 +138,7 @@ class _Step4ViewState extends ConsumerState<Step4View> {
|
|||
Future<bool?> _showSendFromFiroBalanceSelectSheet(String walletId) async {
|
||||
final coin = ref.read(pWalletCoin(walletId));
|
||||
final balancePublic = ref.read(pWalletBalance(walletId));
|
||||
final balancePrivate = ref.read(pWalletBalanceSecondary(walletId));
|
||||
final balancePrivate = ref.read(pWalletBalanceTertiary(walletId));
|
||||
|
||||
return await showModalBottomSheet<bool?>(
|
||||
context: context,
|
||||
|
|
47
lib/pages/frost_mascot.dart
Normal file
47
lib/pages/frost_mascot.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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:flutter/material.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/frost_step_explanation_dialog.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
|
||||
class FrostMascot extends StatelessWidget {
|
||||
final String title;
|
||||
final String body;
|
||||
FrostMascot({
|
||||
super.key,
|
||||
this.onPressed, required this.title, required this.body,
|
||||
});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 24,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
FrostStepExplanationDialog(title: title, body: body),
|
||||
);
|
||||
},
|
||||
child: Image(
|
||||
image: AssetImage(
|
||||
Assets.png.mascot,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
404
lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart
Normal file
404
lib/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart
Normal file
|
@ -0,0 +1,404 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_continue_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class FrostAttemptSignConfigView extends ConsumerStatefulWidget {
|
||||
const FrostAttemptSignConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostAttemptSignConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostAttemptSignConfigView> createState() =>
|
||||
_FrostAttemptSignConfigViewState();
|
||||
}
|
||||
|
||||
class _FrostAttemptSignConfigViewState
|
||||
extends ConsumerState<FrostAttemptSignConfigView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final String myName;
|
||||
late final List<String> participantsWithoutMe;
|
||||
late final String myPreprocess;
|
||||
late final int myIndex;
|
||||
late final int threshold;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
bool hasEnoughPreprocesses() {
|
||||
// own preprocess is not included in controllers and must be set here
|
||||
int count = 1;
|
||||
|
||||
for (final controller in controllers) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count >= threshold;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
final frostInfo = wallet.frostInfo;
|
||||
|
||||
myName = frostInfo.myName;
|
||||
threshold = frostInfo.threshold;
|
||||
participantsWithoutMe = List.from(frostInfo.participants); // Copy so it isn't fixed-length.
|
||||
myIndex = participantsWithoutMe.indexOf(frostInfo.myName);
|
||||
myPreprocess = ref.read(pFrostAttemptSignData.state).state!.preprocess;
|
||||
|
||||
participantsWithoutMe.removeAt(myIndex);
|
||||
|
||||
for (int i = 0; i < participantsWithoutMe.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Preprocesses",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myPreprocess,
|
||||
size: 220,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My name",
|
||||
detail: myName,
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My preprocess",
|
||||
detail: myPreprocess,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myPreprocess,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myPreprocess,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < participantsWithoutMe.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostPreprocessesTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${participantsWithoutMe[i]}'s preprocess",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Preprocess Field Input.",
|
||||
key: Key(
|
||||
"frostPreprocessesClearButtonKey_$i",
|
||||
),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Preprocess Field Input.",
|
||||
key: Key(
|
||||
"frostPreprocessesPasteButtonKey_$i",
|
||||
),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostPreprocessesScanQrButtonKey_$i",
|
||||
),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Continue signing",
|
||||
enabled: hasEnoughPreprocesses(),
|
||||
onPressed: () async {
|
||||
// collect Preprocess strings (not including my own)
|
||||
final preprocesses = controllers.map((e) => e.text).toList();
|
||||
|
||||
// collect participants who are involved in this transaction
|
||||
final List<String> requiredParticipantsUnordered = [];
|
||||
for (int i = 0; i < participantsWithoutMe.length; i++) {
|
||||
if (preprocesses[i].isNotEmpty) {
|
||||
requiredParticipantsUnordered.add(participantsWithoutMe[i]);
|
||||
}
|
||||
}
|
||||
ref.read(pFrostSelectParticipantsUnordered.notifier).state =
|
||||
requiredParticipantsUnordered;
|
||||
|
||||
// insert an empty string at my index
|
||||
preprocesses.insert(myIndex, "");
|
||||
|
||||
try {
|
||||
ref.read(pFrostContinueSignData.notifier).state =
|
||||
Frost.continueSigning(
|
||||
machinePtr:
|
||||
ref.read(pFrostAttemptSignData.state).state!.machinePtr,
|
||||
preprocesses: preprocesses,
|
||||
);
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostContinueSignView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Failed to continue signing",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
206
lib/pages/send_view/frost_ms/frost_complete_sign_view.dart
Normal file
206
lib/pages/send_view/frost_ms/frost_complete_sign_view.dart
Normal file
|
@ -0,0 +1,206 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.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/conditional_parent.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class FrostCompleteSignView extends ConsumerStatefulWidget {
|
||||
const FrostCompleteSignView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostCompleteSignView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostCompleteSignView> createState() =>
|
||||
_FrostCompleteSignViewState();
|
||||
}
|
||||
|
||||
class _FrostCompleteSignViewState extends ConsumerState<FrostCompleteSignView> {
|
||||
bool _broadcastLock = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Preview transaction",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: ref.watch(pFrostTxData.state).state!.raw!,
|
||||
size: 220,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "Raw transaction hex",
|
||||
detail: ref.watch(pFrostTxData.state).state!.raw!,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: ref.watch(pFrostTxData.state).state!.raw!,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: ref.watch(pFrostTxData.state).state!.raw!,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Broadcast Transaction",
|
||||
onPressed: () async {
|
||||
if (_broadcastLock) {
|
||||
return;
|
||||
}
|
||||
_broadcastLock = true;
|
||||
|
||||
try {
|
||||
Exception? ex;
|
||||
final txData = await showLoading(
|
||||
whileFuture: ref
|
||||
.read(pWallets)
|
||||
.getWallet(widget.walletId)
|
||||
.confirmSend(
|
||||
txData: ref.read(pFrostTxData.state).state!,
|
||||
),
|
||||
context: context,
|
||||
message: "Broadcasting transaction to network",
|
||||
isDesktop: Util.isDesktop,
|
||||
onException: (e) {
|
||||
ex = e;
|
||||
},
|
||||
);
|
||||
|
||||
if (ex != null) {
|
||||
throw ex!;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (txData != null) {
|
||||
ref.read(pFrostTxData.state).state = txData;
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(
|
||||
Util.isDesktop
|
||||
? MyStackView.routeName
|
||||
: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Broadcast error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_broadcastLock = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,445 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_complete_sign_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class FrostContinueSignView extends ConsumerStatefulWidget {
|
||||
const FrostContinueSignView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostContinueSignView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostContinueSignView> createState() =>
|
||||
_FrostContinueSignViewState();
|
||||
}
|
||||
|
||||
class _FrostContinueSignViewState extends ConsumerState<FrostContinueSignView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final String myName;
|
||||
late final List<String> participantsWithoutMe;
|
||||
late final List<String> participantsAll;
|
||||
late final String myShare;
|
||||
late final int myIndex;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
|
||||
final frostInfo = wallet.frostInfo;
|
||||
|
||||
myName = frostInfo.myName;
|
||||
participantsAll = frostInfo.participants;
|
||||
myIndex = frostInfo.participants.indexOf(frostInfo.myName);
|
||||
myShare = ref.read(pFrostContinueSignData.state).state!.share;
|
||||
|
||||
participantsWithoutMe = frostInfo.participants
|
||||
.toSet()
|
||||
.intersection(
|
||||
ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet())
|
||||
.toList();
|
||||
|
||||
participantsWithoutMe.remove(myName);
|
||||
|
||||
for (int i = 0; i < participantsWithoutMe.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.transactionCreation,
|
||||
popUntilOnYesRouteName: Util.isDesktop
|
||||
? DesktopWalletView.routeName
|
||||
: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.transactionCreation,
|
||||
popUntilOnYesRouteName: DesktopWalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.transactionCreation,
|
||||
popUntilOnYesRouteName: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Shares",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myShare,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My name",
|
||||
detail: myName,
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My shares",
|
||||
detail: myShare,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myShare,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myShare,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < participantsWithoutMe.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostSharesTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${participantsWithoutMe[i]}'s share",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears "
|
||||
"The Share Field Input.",
|
||||
key: Key(
|
||||
"frostSharesClearButtonKey_$i",
|
||||
),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From "
|
||||
"Clipboard To Share Field Input.",
|
||||
key: Key(
|
||||
"frostSharesPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera "
|
||||
"For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostSharesScanQrButtonKey_$i",
|
||||
),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions "
|
||||
"while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Complete signing",
|
||||
onPressed: () async {
|
||||
// check for empty shares
|
||||
if (controllers
|
||||
.map((e) => e.text.isEmpty)
|
||||
.reduce((value, element) => value |= element)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Missing Shares",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// collect Share strings
|
||||
final sharesCollected =
|
||||
controllers.map((e) => e.text).toList();
|
||||
|
||||
final List<String> shares = [];
|
||||
for (final participant in participantsAll) {
|
||||
if (participantsWithoutMe.contains(participant)) {
|
||||
shares.add(sharesCollected[
|
||||
participantsWithoutMe.indexOf(participant)]);
|
||||
} else {
|
||||
shares.add("");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final rawTx = Frost.completeSigning(
|
||||
machinePtr: ref
|
||||
.read(pFrostContinueSignData.state)
|
||||
.state!
|
||||
.machinePtr,
|
||||
shares: shares,
|
||||
);
|
||||
|
||||
ref.read(pFrostTxData.state).state =
|
||||
ref.read(pFrostTxData.state).state!.copyWith(
|
||||
raw: rawTx,
|
||||
);
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostCompleteSignView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Failed to complete signing process",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
185
lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart
Normal file
185
lib/pages/send_view/frost_ms/frost_create_sign_config_view.dart
Normal file
|
@ -0,0 +1,185 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart';
|
||||
|
||||
class FrostCreateSignConfigView extends ConsumerStatefulWidget {
|
||||
const FrostCreateSignConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostCreateSignConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostCreateSignConfigView> createState() =>
|
||||
_FrostCreateSignConfigViewState();
|
||||
}
|
||||
|
||||
class _FrostCreateSignConfigViewState
|
||||
extends ConsumerState<FrostCreateSignConfigView> {
|
||||
bool _attemptSignLock = false;
|
||||
|
||||
Future<void> _attemptSign() async {
|
||||
if (_attemptSignLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
_attemptSignLock = true;
|
||||
|
||||
try {
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
|
||||
final attemptSignRes = await wallet.frostAttemptSignConfig(
|
||||
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
|
||||
);
|
||||
|
||||
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostAttemptSignConfigView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
} finally {
|
||||
_attemptSignLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double qrImageSize =
|
||||
Util.isDesktop ? 360 : MediaQuery.of(context).size.width - 32;
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
width: 600, // Was 480, may look better but overflows the bottom.
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ConditionalParent(
|
||||
condition: !Util.isDesktop,
|
||||
builder: (child) => Background(
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Sign config",
|
||||
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(),
|
||||
SizedBox(
|
||||
height: qrImageSize,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
|
||||
size: qrImageSize,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Encoded config",
|
||||
detail: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: ref.watch(pFrostTxData.state).state!.frostMSConfig!,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 20 : 16,
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Attempt sign",
|
||||
onPressed: () {
|
||||
_attemptSign();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
332
lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart
Normal file
332
lib/pages/send_view/frost_ms/frost_import_sign_config_view.dart
Normal file
|
@ -0,0 +1,332 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/format.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class FrostImportSignConfigView extends ConsumerStatefulWidget {
|
||||
const FrostImportSignConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostImportSignConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostImportSignConfigView> createState() =>
|
||||
_FrostImportSignConfigViewState();
|
||||
}
|
||||
|
||||
class _FrostImportSignConfigViewState
|
||||
extends ConsumerState<FrostImportSignConfigView> {
|
||||
late final TextEditingController configFieldController;
|
||||
late final FocusNode configFocusNode;
|
||||
|
||||
bool _configEmpty = true;
|
||||
|
||||
bool _attemptSignLock = false;
|
||||
|
||||
Future<void> _attemptSign() async {
|
||||
if (_attemptSignLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
_attemptSignLock = true;
|
||||
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
final config = configFieldController.text;
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
|
||||
final data = Frost.extractDataFromSignConfig(
|
||||
signConfig: config,
|
||||
coin: wallet.cryptoCurrency,
|
||||
);
|
||||
|
||||
final utxos = await ref
|
||||
.read(mainDBProvider)
|
||||
.getUTXOs(wallet.walletId)
|
||||
.filter()
|
||||
.anyOf(
|
||||
data.inputs,
|
||||
(q, e) => q
|
||||
.txidEqualTo(Format.uint8listToString(e.hash))
|
||||
.and()
|
||||
.valueEqualTo(e.value)
|
||||
.and()
|
||||
.voutEqualTo(e.vout))
|
||||
.findAll();
|
||||
|
||||
// TODO add more data from 'data' and display to user ?
|
||||
ref.read(pFrostTxData.notifier).state = TxData(
|
||||
frostMSConfig: config,
|
||||
recipients: data.recipients
|
||||
.map((e) => (address: e.address, amount: e.amount, isChange: false))
|
||||
.toList(),
|
||||
utxos: utxos.toSet(),
|
||||
);
|
||||
|
||||
final attemptSignRes = await wallet.frostAttemptSignConfig(
|
||||
config: ref.read(pFrostTxData.state).state!.frostMSConfig!,
|
||||
);
|
||||
|
||||
ref.read(pFrostAttemptSignData.notifier).state = attemptSignRes;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostAttemptSignConfigView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Import and attempt sign config failed",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_attemptSignLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
configFieldController = TextEditingController();
|
||||
configFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
configFieldController.dispose();
|
||||
configFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Import FROST sign config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frConfigTextFieldKey"),
|
||||
controller: configFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: configFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter config",
|
||||
configFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _configEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_configEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frConfigClearButtonKey"),
|
||||
onTap: () {
|
||||
configFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_configEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||
key: const Key("frConfigPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
configFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _configEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_configEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start signing",
|
||||
enabled: !_configEmpty,
|
||||
onPressed: () {
|
||||
_attemptSign();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
613
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal file
613
lib/pages/send_view/frost_ms/frost_send_view.dart
Normal file
|
@ -0,0 +1,613 @@
|
|||
/*
|
||||
* 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:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||
import 'package:stackwallet/pages/coin_control/coin_control_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/recipient.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
import 'package:stackwallet/themes/coin_icon_provider.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/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.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/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/fee_slider.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class FrostSendView extends ConsumerStatefulWidget {
|
||||
const FrostSendView({
|
||||
Key? key,
|
||||
required this.walletId,
|
||||
required this.coin,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String routeName = "/frostSendView";
|
||||
|
||||
final String walletId;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<FrostSendView> createState() => _FrostSendViewState();
|
||||
}
|
||||
|
||||
class _FrostSendViewState extends ConsumerState<FrostSendView> {
|
||||
final List<int> recipientWidgetIndexes = [0];
|
||||
int _greatestWidgetIndex = 0;
|
||||
|
||||
late final String walletId;
|
||||
late final Coin coin;
|
||||
|
||||
late TextEditingController noteController;
|
||||
late TextEditingController onChainNoteController;
|
||||
|
||||
final _noteFocusNode = FocusNode();
|
||||
|
||||
Set<UTXO> selectedUTXOs = {};
|
||||
|
||||
bool _createSignLock = false;
|
||||
|
||||
Future<TxData> _loadingFuture() async {
|
||||
final wallet = ref.read(pWallets).getWallet(walletId) as BitcoinFrostWallet;
|
||||
|
||||
final recipients = recipientWidgetIndexes
|
||||
.map((i) => ref.read(pRecipient(i).state).state)
|
||||
.map((e) => (address: e!.address, amount: e!.amount!, isChange: false))
|
||||
.toList(growable: false);
|
||||
|
||||
final txData = await wallet.frostCreateSignConfig(
|
||||
txData: TxData(recipients: recipients),
|
||||
changeAddress: (await wallet.getCurrentReceivingAddress())!.value,
|
||||
feePerWeight: customFeeRate,
|
||||
);
|
||||
|
||||
return txData;
|
||||
}
|
||||
|
||||
Future<void> _createSignConfig() async {
|
||||
if (_createSignLock) {
|
||||
return;
|
||||
}
|
||||
_createSignLock = true;
|
||||
|
||||
try {
|
||||
// wait for keyboard to disappear
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
|
||||
TxData? txData;
|
||||
if (mounted) {
|
||||
txData = await showLoading<TxData>(
|
||||
whileFuture: _loadingFuture(),
|
||||
context: context,
|
||||
message: "Generating sign config",
|
||||
isDesktop: Util.isDesktop,
|
||||
onException: (e) {
|
||||
throw e;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted && txData != null) {
|
||||
ref.read(pFrostTxData.notifier).state = txData;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FrostCreateSignConfigView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return StackDialog(
|
||||
title: "Create sign config failed",
|
||||
message: e.toString(),
|
||||
rightButton: TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Ok",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_createSignLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
int customFeeRate = 1;
|
||||
|
||||
void _validateRecipientFormStates() {
|
||||
for (final i in recipientWidgetIndexes) {
|
||||
final state = ref.read(pRecipient(i).state).state;
|
||||
if (state?.amount == null || state?.address == null) {
|
||||
ref.read(previewTxButtonStateProvider.notifier).state = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
ref.read(previewTxButtonStateProvider.notifier).state = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
coin = widget.coin;
|
||||
walletId = widget.walletId;
|
||||
|
||||
noteController = TextEditingController();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
noteController.dispose();
|
||||
|
||||
_noteFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
final wallet = ref.watch(pWallets).getWallet(walletId);
|
||||
|
||||
final showCoinControl = wallet is CoinControlInterface &&
|
||||
ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.enableCoinControl,
|
||||
),
|
||||
);
|
||||
|
||||
return ConditionalParent(
|
||||
condition: !Util.isDesktop,
|
||||
builder: (child) => Background(
|
||||
child: Scaffold(
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Send ${coin.ticker}",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: AppBarIconButton(
|
||||
semanticsLabel: "Import sign config Button.",
|
||||
key: const Key("importSignConfigButtonKey"),
|
||||
size: 36,
|
||||
shadows: const [],
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.circlePlus,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
FrostImportSignConfigView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
// subtract top and bottom padding set in parent
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!Util.isDesktop)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.file(
|
||||
File(
|
||||
ref.watch(
|
||||
coinIconProvider(coin),
|
||||
),
|
||||
),
|
||||
width: 22,
|
||||
height: 22,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 6,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
ref.watch(pWalletName(walletId)),
|
||||
style: STextStyles.titleBold12(context)
|
||||
.copyWith(fontSize: 14),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
// const SizedBox(
|
||||
// height: 2,
|
||||
// ),
|
||||
Text(
|
||||
"Available balance",
|
||||
style: STextStyles.label(context)
|
||||
.copyWith(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
Util.isDesktop
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
)
|
||||
: const Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// cryptoAmountController.text = ref
|
||||
// .read(pAmountFormatter(coin))
|
||||
// .format(
|
||||
// _cachedBalance!,
|
||||
// withUnitName: false,
|
||||
// );
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
ref.watch(pAmountFormatter(coin)).format(ref
|
||||
.watch(pWalletBalance(walletId))
|
||||
.spendable),
|
||||
style:
|
||||
STextStyles.titleBold12(context).copyWith(
|
||||
fontSize: 10,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
// Text(
|
||||
// "${(manager.balance.spendable.decimal * ref.watch(
|
||||
// priceAnd24hChangeNotifierProvider.select(
|
||||
// (value) => value.getPrice(coin).item1,
|
||||
// ),
|
||||
// )).toAmount(
|
||||
// fractionDigits: 2,
|
||||
// ).fiatString(
|
||||
// locale: locale,
|
||||
// )} ${ref.watch(
|
||||
// prefsChangeNotifierProvider
|
||||
// .select((value) => value.currency),
|
||||
// )}",
|
||||
// style: STextStyles.subtitle(context).copyWith(
|
||||
// fontSize: 8,
|
||||
// ),
|
||||
// textAlign: TextAlign.right,
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Recipients",
|
||||
style: STextStyles.smallMed12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
CustomTextButton(
|
||||
text: "Add",
|
||||
onTap: () {
|
||||
// used for tracking recipient forms
|
||||
_greatestWidgetIndex++;
|
||||
recipientWidgetIndexes.add(_greatestWidgetIndex);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
for (int i = 0; i < recipientWidgetIndexes.length; i++)
|
||||
ConditionalParent(
|
||||
condition: recipientWidgetIndexes.length > 1,
|
||||
builder: (child) => Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: child,
|
||||
),
|
||||
child: Recipient(
|
||||
key: Key(
|
||||
"recipientKey_${recipientWidgetIndexes[i]}",
|
||||
),
|
||||
index: recipientWidgetIndexes[i],
|
||||
coin: coin,
|
||||
onChanged: () {
|
||||
_validateRecipientFormStates();
|
||||
},
|
||||
remove: i == 0 && recipientWidgetIndexes.length == 1
|
||||
? null
|
||||
: () {
|
||||
recipientWidgetIndexes.removeAt(i);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showCoinControl)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (showCoinControl)
|
||||
RoundedWhiteContainer(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Coin control",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textSubtitle1,
|
||||
),
|
||||
),
|
||||
CustomTextButton(
|
||||
text: selectedUTXOs.isEmpty
|
||||
? "Select coins"
|
||||
: "Selected coins (${selectedUTXOs.length})",
|
||||
onTap: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 100),
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// finally spendable = ref
|
||||
// .read(walletsChangeNotifierProvider)
|
||||
// .getManager(widget.walletId)
|
||||
// .balance
|
||||
// .spendable;
|
||||
|
||||
// TODO: [prio=high] make sure this coincontrol works correctly
|
||||
|
||||
Amount? amount;
|
||||
|
||||
final result = await Navigator.of(context).pushNamed(
|
||||
CoinControlView.routeName,
|
||||
arguments: Tuple4(
|
||||
walletId,
|
||||
CoinControlViewType.use,
|
||||
amount,
|
||||
selectedUTXOs,
|
||||
),
|
||||
);
|
||||
|
||||
if (result is Set<UTXO>) {
|
||||
setState(() {
|
||||
selectedUTXOs = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Text(
|
||||
"Note (optional)",
|
||||
style: STextStyles.smallMed12(context),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
autocorrect: Util.isDesktop ? false : true,
|
||||
enableSuggestions: Util.isDesktop ? false : true,
|
||||
controller: noteController,
|
||||
focusNode: _noteFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: standardInputDecoration(
|
||||
"Type something...",
|
||||
_noteFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
suffixIcon: noteController.text.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
children: [
|
||||
TextFieldIconButton(
|
||||
child: const XIcon(),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
noteController.text = "";
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 12,
|
||||
top: 16,
|
||||
),
|
||||
child: FeeSlider(
|
||||
coin: coin,
|
||||
onSatVByteChanged: (rate) {
|
||||
customFeeRate = rate;
|
||||
},
|
||||
),
|
||||
),
|
||||
Util.isDesktop
|
||||
? const SizedBox(
|
||||
height: 12,
|
||||
)
|
||||
: const Spacer(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: ref.watch(previewTxButtonStateProvider.state).state
|
||||
? _createSignConfig
|
||||
: null,
|
||||
style: ref.watch(previewTxButtonStateProvider.state).state
|
||||
? Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context)
|
||||
: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryDisabledButtonStyle(context),
|
||||
child: Text(
|
||||
"Create config",
|
||||
style: STextStyles.button(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final previewTxButtonStateProvider = StateProvider((_) => false);
|
502
lib/pages/send_view/frost_ms/recipient.dart
Normal file
502
lib/pages/send_view/frost_ms/recipient.dart
Normal file
|
@ -0,0 +1,502 @@
|
|||
import 'package:decimal/decimal.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/providers/global/locale_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/address_utils.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_formatter.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_input_formatter.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount_unit.dart';
|
||||
import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
|
||||
import 'package:stackwallet/utilities/clipboard_interface.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/custom_buttons/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
//TODO: move the following two providers elsewhere
|
||||
final pClipboard =
|
||||
Provider<ClipboardInterface>((ref) => const ClipboardWrapper());
|
||||
final pBarcodeScanner =
|
||||
Provider<BarcodeScannerInterface>((ref) => const BarcodeScannerWrapper());
|
||||
|
||||
// final _pPrice = Provider.family<Decimal, Coin>((ref, coin) {
|
||||
// return ref.watch(
|
||||
// priceAnd24hChangeNotifierProvider
|
||||
// .select((value) => value.getPrice(coin).item1),
|
||||
// );
|
||||
// });
|
||||
|
||||
final pRecipient =
|
||||
StateProvider.family<({String address, Amount? amount})?, int>(
|
||||
(ref, index) => null);
|
||||
|
||||
class Recipient extends ConsumerStatefulWidget {
|
||||
const Recipient({
|
||||
super.key,
|
||||
required this.index,
|
||||
required this.coin,
|
||||
this.remove,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
final int index;
|
||||
final Coin coin;
|
||||
|
||||
final VoidCallback? remove;
|
||||
final VoidCallback? onChanged;
|
||||
|
||||
@override
|
||||
ConsumerState<Recipient> createState() => _RecipientState();
|
||||
}
|
||||
|
||||
class _RecipientState extends ConsumerState<Recipient> {
|
||||
late final TextEditingController addressController, amountController;
|
||||
late final FocusNode addressFocusNode, amountFocusNode;
|
||||
|
||||
bool _addressIsEmpty = true;
|
||||
bool _cryptoAmountChangeLock = false;
|
||||
|
||||
void _updateRecipientData() {
|
||||
final address = addressController.text;
|
||||
final amount =
|
||||
ref.read(pAmountFormatter(widget.coin)).tryParse(amountController.text);
|
||||
|
||||
ref.read(pRecipient(widget.index).notifier).state = (
|
||||
address: address,
|
||||
amount: amount,
|
||||
);
|
||||
widget.onChanged?.call();
|
||||
}
|
||||
|
||||
void _cryptoAmountChanged() async {
|
||||
if (!_cryptoAmountChangeLock) {
|
||||
Amount? cryptoAmount = ref.read(pAmountFormatter(widget.coin)).tryParse(
|
||||
amountController.text,
|
||||
);
|
||||
if (cryptoAmount != null) {
|
||||
if (ref.read(pRecipient(widget.index))?.amount != null &&
|
||||
ref.read(pRecipient(widget.index))?.amount == cryptoAmount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// final price = ref.read(_pPrice(widget.coin));
|
||||
//
|
||||
// if (price > Decimal.zero) {
|
||||
// baseController.text = (cryptoAmount.decimal * price)
|
||||
// .toAmount(
|
||||
// fractionDigits: 2,
|
||||
// )
|
||||
// .fiatString(
|
||||
// locale: ref.read(localeServiceChangeNotifierProvider).locale,
|
||||
// );
|
||||
// }
|
||||
} else {
|
||||
cryptoAmount = null;
|
||||
// baseController.text = "";
|
||||
}
|
||||
|
||||
_updateRecipientData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
addressController = TextEditingController();
|
||||
amountController = TextEditingController();
|
||||
// baseController = TextEditingController();
|
||||
|
||||
addressFocusNode = FocusNode();
|
||||
amountFocusNode = FocusNode();
|
||||
// baseFocusNode = FocusNode();
|
||||
|
||||
amountController.addListener(_cryptoAmountChanged);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
amountController.removeListener(_cryptoAmountChanged);
|
||||
|
||||
addressController.dispose();
|
||||
amountController.dispose();
|
||||
// baseController.dispose();
|
||||
|
||||
addressFocusNode.dispose();
|
||||
amountFocusNode.dispose();
|
||||
// baseFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String locale = ref.watch(
|
||||
localeServiceChangeNotifierProvider.select(
|
||||
(value) => value.locale,
|
||||
),
|
||||
);
|
||||
|
||||
return RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("sendViewAddressFieldKey"),
|
||||
controller: addressController,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
focusNode: addressFocusNode,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_addressIsEmpty = addressController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter ${widget.coin.ticker} address",
|
||||
addressFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _addressIsEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_addressIsEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Address Field Input.",
|
||||
key: const Key(
|
||||
"sendViewClearAddressFieldButtonKey"),
|
||||
onTap: () {
|
||||
addressController.text = "";
|
||||
|
||||
setState(() {
|
||||
_addressIsEmpty = true;
|
||||
});
|
||||
|
||||
_updateRecipientData();
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Address Field Input.",
|
||||
key: const Key(
|
||||
"sendViewPasteAddressFieldButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data = await ref
|
||||
.read(pClipboard)
|
||||
.getData(Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
String content = data.text!.trim();
|
||||
if (content.contains("\n")) {
|
||||
content = content.substring(
|
||||
0, content.indexOf("\n"));
|
||||
}
|
||||
|
||||
addressController.text = content.trim();
|
||||
|
||||
setState(() {
|
||||
_addressIsEmpty =
|
||||
addressController.text.isEmpty;
|
||||
});
|
||||
|
||||
_updateRecipientData();
|
||||
}
|
||||
},
|
||||
child: _addressIsEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_addressIsEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel: "Scan QR Button. "
|
||||
"Opens Camera For Scanning QR Code.",
|
||||
key: const Key(
|
||||
"sendViewScanQrButtonKey",
|
||||
),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await ref.read(pBarcodeScanner).scan();
|
||||
|
||||
Logging.instance.log(
|
||||
"qrResult content: ${qrResult.rawContent}",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
/// TODO: deal with address utils
|
||||
final results =
|
||||
AddressUtils.parseUri(qrResult.rawContent);
|
||||
|
||||
Logging.instance.log(
|
||||
"qrResult parsed: $results",
|
||||
level: LogLevel.Info,
|
||||
);
|
||||
|
||||
if (results.isNotEmpty &&
|
||||
results["scheme"] ==
|
||||
widget.coin.uriScheme) {
|
||||
// auto fill address
|
||||
|
||||
addressController.text =
|
||||
(results["address"] ?? "").trim();
|
||||
|
||||
// autofill amount field
|
||||
if (results["amount"] != null) {
|
||||
final Amount amount =
|
||||
Decimal.parse(results["amount"]!)
|
||||
.toAmount(
|
||||
fractionDigits: widget.coin.decimals,
|
||||
);
|
||||
amountController.text = ref
|
||||
.read(pAmountFormatter(widget.coin))
|
||||
.format(
|
||||
amount,
|
||||
withUnitName: false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
addressController.text =
|
||||
qrResult.rawContent.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_addressIsEmpty =
|
||||
addressController.text.isEmpty;
|
||||
});
|
||||
|
||||
_updateRecipientData();
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while "
|
||||
"trying to scan qr code in SendView: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.smallMed14(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
),
|
||||
key: const Key("amountInputFieldCryptoTextFieldKey"),
|
||||
controller: amountController,
|
||||
focusNode: amountFocusNode,
|
||||
keyboardType: Util.isDesktop
|
||||
? null
|
||||
: const TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: true,
|
||||
),
|
||||
textAlign: TextAlign.right,
|
||||
inputFormatters: [
|
||||
AmountInputFormatter(
|
||||
decimals: widget.coin.decimals,
|
||||
unit: ref.watch(pAmountUnit(widget.coin)),
|
||||
locale: locale,
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
right: 12,
|
||||
),
|
||||
hintText: "0",
|
||||
hintStyle: STextStyles.fieldLabel(context).copyWith(
|
||||
fontSize: 14,
|
||||
),
|
||||
prefixIcon: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
ref
|
||||
.watch(pAmountUnit(widget.coin))
|
||||
.unitForCoin(widget.coin),
|
||||
style: STextStyles.smallMed14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (ref.watch(prefsChangeNotifierProvider
|
||||
// .select((value) => value.externalCalls)))
|
||||
// const SizedBox(
|
||||
// height: 8,
|
||||
// ),
|
||||
// if (ref.watch(prefsChangeNotifierProvider
|
||||
// .select((value) => value.externalCalls)))
|
||||
// TextField(
|
||||
// autocorrect: Util.isDesktop ? false : true,
|
||||
// enableSuggestions: Util.isDesktop ? false : true,
|
||||
// style: STextStyles.smallMed14(context).copyWith(
|
||||
// color: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
// ),
|
||||
// key: const Key("amountInputFieldFiatTextFieldKey"),
|
||||
// controller: baseController,
|
||||
// focusNode: baseFocusNode,
|
||||
// keyboardType: Util.isDesktop
|
||||
// ? null
|
||||
// : const TextInputType.numberWithOptions(
|
||||
// signed: false,
|
||||
// decimal: true,
|
||||
// ),
|
||||
// textAlign: TextAlign.right,
|
||||
// inputFormatters: [
|
||||
// AmountInputFormatter(
|
||||
// decimals: 2,
|
||||
// locale: locale,
|
||||
// ),
|
||||
// ],
|
||||
// onChanged: (baseAmountString) {
|
||||
// final baseAmount = Amount.tryParseFiatString(
|
||||
// baseAmountString,
|
||||
// locale: locale,
|
||||
// );
|
||||
// Amount? cryptoAmount;
|
||||
// final int decimals = widget.coin.decimals;
|
||||
// if (baseAmount != null) {
|
||||
// final _price = ref.read(_pPrice(widget.coin));
|
||||
//
|
||||
// if (_price == Decimal.zero) {
|
||||
// cryptoAmount = 0.toAmountAsRaw(
|
||||
// fractionDigits: decimals,
|
||||
// );
|
||||
// } else {
|
||||
// cryptoAmount = baseAmount <= Amount.zero
|
||||
// ? 0.toAmountAsRaw(fractionDigits: decimals)
|
||||
// : (baseAmount.decimal / _price)
|
||||
// .toDecimal(
|
||||
// scaleOnInfinitePrecision: decimals,
|
||||
// )
|
||||
// .toAmount(fractionDigits: decimals);
|
||||
// }
|
||||
// if (ref.read(pRecipient(widget.index))?.amount != null &&
|
||||
// ref.read(pRecipient(widget.index))?.amount ==
|
||||
// cryptoAmount) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// final amountString =
|
||||
// ref.read(pAmountFormatter(widget.coin)).format(
|
||||
// cryptoAmount,
|
||||
// withUnitName: false,
|
||||
// );
|
||||
//
|
||||
// _cryptoAmountChangeLock = true;
|
||||
// amountController.text = amountString;
|
||||
// _cryptoAmountChangeLock = false;
|
||||
// } else {
|
||||
// cryptoAmount = 0.toAmountAsRaw(
|
||||
// fractionDigits: decimals,
|
||||
// );
|
||||
// _cryptoAmountChangeLock = true;
|
||||
// amountController.text = "";
|
||||
// _cryptoAmountChangeLock = false;
|
||||
// }
|
||||
//
|
||||
// _updateRecipientData();
|
||||
// },
|
||||
// decoration: InputDecoration(
|
||||
// contentPadding: const EdgeInsets.only(
|
||||
// top: 12,
|
||||
// right: 12,
|
||||
// ),
|
||||
// hintText: "0",
|
||||
// hintStyle: STextStyles.fieldLabel(context).copyWith(
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// prefixIcon: FittedBox(
|
||||
// fit: BoxFit.scaleDown,
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Text(
|
||||
// ref.watch(prefsChangeNotifierProvider
|
||||
// .select((value) => value.currency)),
|
||||
// style: STextStyles.smallMed14(context).copyWith(
|
||||
// color: Theme.of(context)
|
||||
// .extension<StackColors>()!
|
||||
// .accentColorDark),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
if (widget.remove != null)
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
if (widget.remove != null)
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
CustomTextButton(
|
||||
text: "Remove",
|
||||
onTap: () {
|
||||
ref.read(pRecipient(widget.index).notifier).state = null;
|
||||
widget.remove?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import 'package:stackwallet/utilities/text_styles.dart';
|
|||
import 'package:stackwallet/wallets/isar/providers/eth/current_token_wallet_provider.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
|
||||
import 'package:stackwallet/widgets/animated_text.dart';
|
||||
|
||||
final feeSheetSessionCacheProvider =
|
||||
|
@ -697,7 +698,7 @@ class _TransactionFeeSelectionSheetState
|
|||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (coin.isElectrumXCoin)
|
||||
if (wallet is ElectrumXInterface)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final state =
|
||||
|
@ -766,7 +767,7 @@ class _TransactionFeeSelectionSheetState
|
|||
),
|
||||
),
|
||||
),
|
||||
if (coin.isElectrumXCoin)
|
||||
if (wallet is ElectrumXInterface)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
|
|
|
@ -10,11 +10,10 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/providers/global/debug_service_provider.dart';
|
||||
import 'package:stackwallet/providers/providers.dart';
|
||||
|
@ -22,10 +21,8 @@ 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/background.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/onetime_popups/tor_has_been_add_dialog.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class HiddenSettings extends StatelessWidget {
|
||||
|
@ -39,27 +36,25 @@ class HiddenSettings extends StatelessWidget {
|
|||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: AppBar(
|
||||
leading: Util.isDesktop
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AppBarIconButton(
|
||||
size: 32,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
shadows: const [],
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.arrowLeft,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AppBarIconButton(
|
||||
size: 32,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
shadows: const [],
|
||||
icon: SvgPicture.asset(
|
||||
Assets.svg.arrowLeft,
|
||||
width: 18,
|
||||
height: 18,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.topNavIconPrimary,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"Dev options",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
|
@ -176,49 +171,48 @@ class HiddenSettings extends StatelessWidget {
|
|||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Consumer(builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showOneTimeTorHasBeenAddedDialogIfRequired(
|
||||
context,
|
||||
);
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Test tor stacy popup",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Consumer(builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final box = await Hive.openBox<bool>(
|
||||
DB.boxNameOneTimeDialogsShown);
|
||||
await box.clear();
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Reset tor stacy popup",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
// Consumer(builder: (_, ref, __) {
|
||||
// return GestureDetector(
|
||||
// onTap: () async {
|
||||
// await showOneTimeTorHasBeenAddedDialogIfRequired(
|
||||
// context,
|
||||
// );
|
||||
// },
|
||||
// child: RoundedWhiteContainer(
|
||||
// child: Text(
|
||||
// "Test tor stacy popup",
|
||||
// style: STextStyles.button(context).copyWith(
|
||||
// color: Theme.of(context)
|
||||
// .extension<StackColors>()!
|
||||
// .accentColorDark),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// const SizedBox(
|
||||
// height: 12,
|
||||
// ),
|
||||
// Consumer(builder: (_, ref, __) {
|
||||
// return GestureDetector(
|
||||
// onTap: () async {
|
||||
// final box = await Hive.openBox<bool>(
|
||||
// DB.boxNameOneTimeDialogsShown);
|
||||
// await box.clear();
|
||||
// },
|
||||
// child: RoundedWhiteContainer(
|
||||
// child: Text(
|
||||
// "Reset tor stacy popup",
|
||||
// style: STextStyles.button(context).copyWith(
|
||||
// color: Theme.of(context)
|
||||
// .extension<StackColors>()!
|
||||
// .accentColorDark),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// const SizedBox(
|
||||
// height: 12,
|
||||
// ),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
if (ref.watch(prefsChangeNotifierProvider
|
||||
|
@ -252,6 +246,36 @@ class HiddenSettings extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.frostEnabled =
|
||||
!(ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.frostEnabled);
|
||||
if (kDebugMode) {
|
||||
print(
|
||||
"FROST enabled: ${ref.read(prefsChangeNotifierProvider).frostEnabled}");
|
||||
}
|
||||
},
|
||||
child: RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Toggle FROST multisig",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Consumer(
|
||||
builder: (_, ref, __) {
|
||||
return GestureDetector(
|
||||
|
|
|
@ -166,6 +166,8 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
case Coin.firo:
|
||||
case Coin.namecoin:
|
||||
case Coin.particl:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
|
@ -177,6 +179,7 @@ class _AddEditNodeViewState extends ConsumerState<AddEditNodeView> {
|
|||
useSSL: formData.useSSL!,
|
||||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -757,6 +760,8 @@ class _NodeFormState extends ConsumerState<NodeForm> {
|
|||
case Coin.eCash:
|
||||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return false;
|
||||
|
||||
case Coin.ethereum:
|
||||
|
|
|
@ -148,12 +148,15 @@ class _NodeDetailsViewState extends ConsumerState<NodeDetailsView> {
|
|||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
case Coin.eCash:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
final client = ElectrumXClient(
|
||||
host: node!.host,
|
||||
port: node.port,
|
||||
useSSL: node.useSSL,
|
||||
failovers: [],
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
coin: coin,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:frostdart/frostdart.dart' as frost;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:stack_wallet_backup/stack_wallet_backup.dart';
|
||||
import 'package:stackwallet/db/hive/db.dart';
|
||||
|
@ -26,6 +27,7 @@ import 'package:stackwallet/models/stack_restoring_ui_state.dart';
|
|||
import 'package:stackwallet/models/trade_wallet_lookup.dart';
|
||||
import 'package:stackwallet/models/wallet_restore_state.dart';
|
||||
import 'package:stackwallet/services/address_book_service.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/services/node_service.dart';
|
||||
import 'package:stackwallet/services/trade_notes_service.dart';
|
||||
import 'package:stackwallet/services/trade_sent_from_stack_service.dart';
|
||||
|
@ -41,7 +43,9 @@ import 'package:stackwallet/utilities/format.dart';
|
|||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart';
|
||||
|
@ -302,6 +306,24 @@ abstract class SWB {
|
|||
await wallet.getMnemonicPassphrase();
|
||||
} else if (wallet is PrivateKeyInterface) {
|
||||
backupWallet['privateKey'] = await wallet.getPrivateKey();
|
||||
} else if (wallet is BitcoinFrostWallet) {
|
||||
String? keys = await wallet.getSerializedKeys();
|
||||
String? config = await wallet.getMultisigConfig();
|
||||
if (keys == null || config == null) {
|
||||
String err = "${wallet.info.coin.name} wallet ${wallet.info.name} "
|
||||
"has null keys or config";
|
||||
Logging.instance.log(err, level: LogLevel.Fatal);
|
||||
throw Exception(err);
|
||||
}
|
||||
//This case should never actually happen in practice unless the whole
|
||||
// wallet is somehow corrupt
|
||||
// TODO [prio=low]: solve case in which either keys or config is null.
|
||||
|
||||
// Format keys & config as a JSON string and set otherDataJsonString.
|
||||
Map<String, dynamic> frostData = {};
|
||||
frostData["keys"] = keys;
|
||||
frostData["config"] = config;
|
||||
backupWallet['frostWalletData'] = jsonEncode(frostData);
|
||||
}
|
||||
backupWallet['coinName'] = wallet.info.coin.name;
|
||||
backupWallet['storedChainHeight'] = wallet.info.cachedChainHeight;
|
||||
|
@ -384,7 +406,9 @@ abstract class SWB {
|
|||
|
||||
if (walletbackup['mnemonic'] == null) {
|
||||
// probably private key based
|
||||
privateKey = walletbackup['privateKey'] as String;
|
||||
if (walletbackup['privateKey'] != null) {
|
||||
privateKey = walletbackup['privateKey'] as String;
|
||||
}
|
||||
} else {
|
||||
if (walletbackup['mnemonic'] is List) {
|
||||
List<String> mnemonicList = (walletbackup['mnemonic'] as List<dynamic>)
|
||||
|
@ -406,6 +430,37 @@ abstract class SWB {
|
|||
);
|
||||
|
||||
try {
|
||||
String? serializedKeys;
|
||||
String? multisigConfig;
|
||||
if (info.coin.isFrost) {
|
||||
// Decode info.otherDataJsonString for Frost recovery info.
|
||||
final frostData = jsonDecode(walletbackup["frostWalletData"] as String);
|
||||
serializedKeys = frostData["keys"] as String;
|
||||
multisigConfig = frostData["config"] as String;
|
||||
|
||||
final myNameIndex = frost.getParticipantIndexFromKeys(
|
||||
serializedKeys: serializedKeys,
|
||||
);
|
||||
final participants = Frost.getParticipants(
|
||||
multisigConfig: multisigConfig,
|
||||
);
|
||||
final myName = participants[myNameIndex];
|
||||
|
||||
final frostInfo = FrostWalletInfo(
|
||||
walletId: info.walletId,
|
||||
knownSalts: [],
|
||||
participants: participants,
|
||||
myName: myName,
|
||||
threshold: frost.multisigThreshold(
|
||||
multisigConfig: multisigConfig,
|
||||
),
|
||||
);
|
||||
|
||||
await MainDB.instance.isar.writeTxn(() async {
|
||||
await MainDB.instance.isar.frostWalletInfo.put(frostInfo);
|
||||
});
|
||||
}
|
||||
|
||||
final wallet = await Wallet.create(
|
||||
walletInfo: info,
|
||||
mainDB: MainDB.instance,
|
||||
|
@ -427,7 +482,15 @@ abstract class SWB {
|
|||
Future<void>? restoringFuture;
|
||||
|
||||
if (!(wallet is CwBasedInterface || wallet is EpiccashWallet)) {
|
||||
restoringFuture = wallet.recover(isRescan: false);
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
restoringFuture = wallet.recover(
|
||||
isRescan: false,
|
||||
multisigConfig: multisigConfig!,
|
||||
serializedKeys: serializedKeys!,
|
||||
);
|
||||
} else {
|
||||
restoringFuture = wallet.recover(isRescan: false);
|
||||
}
|
||||
}
|
||||
|
||||
uiState?.update(
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/rounded_white_container.dart';
|
||||
|
||||
class FrostMSWalletOptionsView extends ConsumerWidget {
|
||||
const FrostMSWalletOptionsView({
|
||||
Key? key,
|
||||
required this.walletId,
|
||||
}) : super(key: key);
|
||||
|
||||
static const String routeName = "/frostMSWalletOptionsView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"FROST Multisig options",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
),
|
||||
body: child),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
left: 16,
|
||||
right: 16,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_OptionButton(
|
||||
label: "Show participants",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
FrostParticipantsView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
_OptionButton(
|
||||
label: "Initiate resharing",
|
||||
onPressed: () {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(walletId)!;
|
||||
|
||||
ref.read(pFrostMyName.state).state = frostInfo.myName;
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
BeginReshareConfigView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
_OptionButton(
|
||||
label: "Import reshare config",
|
||||
onPressed: () {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(walletId)!;
|
||||
|
||||
ref.read(pFrostMyName.state).state = frostInfo.myName;
|
||||
|
||||
Navigator.of(context).pushNamed(
|
||||
ImportReshareConfigView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionButton extends StatelessWidget {
|
||||
const _OptionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedWhiteContainer(
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RawMaterialButton(
|
||||
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onPressed: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 20,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: STextStyles.titleBold12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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';
|
||||
|
||||
class FrostParticipantsView extends ConsumerWidget {
|
||||
const FrostParticipantsView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/frostParticipantsView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(walletId)!;
|
||||
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Participants",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < frostInfo.participants.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Index $i",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
),
|
||||
SelectableText(
|
||||
frostInfo.participants[i] == frostInfo.myName
|
||||
? "${frostInfo.participants[i]} (me)"
|
||||
: frostInfo.participants[i],
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,437 @@
|
|||
import 'dart:ffi';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class FinishResharingView extends ConsumerStatefulWidget {
|
||||
const FinishResharingView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/finishResharingView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<FinishResharingView> createState() =>
|
||||
_FinishResharingViewState();
|
||||
}
|
||||
|
||||
class _FinishResharingViewState extends ConsumerState<FinishResharingView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<int> resharerIndexes;
|
||||
late final String myName;
|
||||
late final int? myResharerIndexIndex;
|
||||
late final String? myResharerComplete;
|
||||
late final bool amOutgoingParticipant;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
bool _buttonLock = false;
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
if (amOutgoingParticipant) {
|
||||
ref.read(pFrostResharingData).reset();
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(
|
||||
Util.isDesktop ? DesktopWalletView.routeName : WalletView.routeName,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// collect resharer completes strings and insert my own at the correct index
|
||||
final resharerCompletes = controllers.map((e) => e.text).toList();
|
||||
if (myResharerIndexIndex != null && myResharerComplete != null) {
|
||||
resharerCompletes.insert(myResharerIndexIndex!, myResharerComplete!);
|
||||
}
|
||||
|
||||
final data = Frost.finishReshared(
|
||||
prior: ref.read(pFrostResharingData).startResharedData!.prior.ref,
|
||||
resharerCompletes: resharerCompletes,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).newWalletData = data;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
VerifyUpdatedWalletView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final amNewParticipant =
|
||||
ref.read(pFrostResharingData).startResharerData == null &&
|
||||
ref.read(pFrostResharingData).incompleteWallet != null &&
|
||||
ref.read(pFrostResharingData).incompleteWallet?.walletId ==
|
||||
widget.walletId;
|
||||
|
||||
myName = ref.read(pFrostResharingData).myName!;
|
||||
|
||||
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||
|
||||
if (amNewParticipant) {
|
||||
myResharerComplete = null;
|
||||
myResharerIndexIndex = null;
|
||||
amOutgoingParticipant = false;
|
||||
} else {
|
||||
myResharerComplete = ref.read(pFrostResharingData).resharerComplete!;
|
||||
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
final myOldIndex =
|
||||
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||
|
||||
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
|
||||
if (myResharerIndexIndex! >= 0) {
|
||||
// remove my name for now as we don't need a text field for it
|
||||
resharerIndexes.removeAt(myResharerIndexIndex!);
|
||||
}
|
||||
|
||||
amOutgoingParticipant = !ref
|
||||
.read(pFrostResharingData)
|
||||
.configData!
|
||||
.newParticipants
|
||||
.contains(ref.read(pFrostResharingData).myName!);
|
||||
}
|
||||
|
||||
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Resharer completes",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (myResharerComplete != null)
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myResharerComplete!,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (myResharerComplete != null) const _Div(),
|
||||
if (myResharerComplete != null)
|
||||
DetailItem(
|
||||
title: "My resharer complete",
|
||||
detail: myResharerComplete!,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myResharerComplete!,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myResharerComplete!,
|
||||
),
|
||||
),
|
||||
if (!amOutgoingParticipant) const _Div(),
|
||||
if (!amOutgoingParticipant)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < resharerIndexes.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostEncryptionKeyTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter index "
|
||||
"${resharerIndexes[i]}"
|
||||
"'s resharer complete",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Encryption Key Field Input.",
|
||||
key: Key(
|
||||
"frostEncryptionKeyClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
|
||||
key: Key(
|
||||
"frostEncryptionKeyPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel: "Scan QR Button. "
|
||||
"Opens Camera For Scanning QR Code.",
|
||||
key: Key("frostScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions "
|
||||
"while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: amOutgoingParticipant ? "Exit" : "Complete",
|
||||
enabled: amOutgoingParticipant ||
|
||||
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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';
|
||||
|
||||
final class BeginReshareConfigView extends ConsumerStatefulWidget {
|
||||
const BeginReshareConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/beginReshareConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<BeginReshareConfigView> createState() =>
|
||||
_BeginReshareConfigViewState();
|
||||
}
|
||||
|
||||
class _BeginReshareConfigViewState
|
||||
extends ConsumerState<BeginReshareConfigView> {
|
||||
late final int currentThreshold;
|
||||
late final List<String> currentParticipants;
|
||||
|
||||
final Map<String, int> pFrostResharersMap = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
ref.read(pFrostResharingData).reset();
|
||||
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
|
||||
currentThreshold = frostInfo.threshold;
|
||||
currentParticipants = frostInfo.participants;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
// title: Text(
|
||||
// "Modify Participants",
|
||||
// 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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Select participants for resharing",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
for (int i = 0; i < currentParticipants.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
),
|
||||
child: RawMaterialButton(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
if (pFrostResharersMap[currentParticipants[i]] ==
|
||||
null) {
|
||||
pFrostResharersMap[currentParticipants[i]] = i;
|
||||
} else {
|
||||
pFrostResharersMap.remove(currentParticipants[i]);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: IgnorePointer(
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: pFrostResharersMap[
|
||||
currentParticipants[i]] ==
|
||||
i,
|
||||
onChanged: (bool? value) {},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
currentParticipants[i],
|
||||
style: STextStyles.itemSubtitle12(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: pFrostResharersMap.length >= currentThreshold,
|
||||
onPressed: () async {
|
||||
await Navigator.of(context).pushNamed(
|
||||
CompleteReshareConfigView.routeName,
|
||||
arguments: (
|
||||
walletId: widget.walletId,
|
||||
resharers:
|
||||
pFrostResharersMap.values.toList(growable: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:frostdart/frostdart.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/format.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/stack_dialog.dart';
|
||||
|
||||
final class CompleteReshareConfigView extends ConsumerStatefulWidget {
|
||||
const CompleteReshareConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
required this.resharers,
|
||||
});
|
||||
|
||||
static const String routeName = "/completeReshareConfigView";
|
||||
|
||||
final String walletId;
|
||||
final List<int> resharers;
|
||||
|
||||
@override
|
||||
ConsumerState<CompleteReshareConfigView> createState() =>
|
||||
_CompleteReshareConfigViewState();
|
||||
}
|
||||
|
||||
class _CompleteReshareConfigViewState
|
||||
extends ConsumerState<CompleteReshareConfigView> {
|
||||
final _newThresholdController = TextEditingController();
|
||||
final _newParticipantsCountController = TextEditingController();
|
||||
|
||||
final List<TextEditingController> controllers = [];
|
||||
|
||||
int _participantsCount = 0;
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
final validationMessage = _validateInputData();
|
||||
|
||||
if (validationMessage != "valid") {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: validationMessage,
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final config = Frost.createResharerConfig(
|
||||
newThreshold: int.parse(_newThresholdController.text),
|
||||
resharers: widget.resharers,
|
||||
newParticipants: controllers.map((e) => e.text).toList(),
|
||||
);
|
||||
|
||||
final salt = Format.uint8listToString(
|
||||
resharerSalt(resharerConfig: config),
|
||||
);
|
||||
|
||||
if (frostInfo.knownSalts.contains(salt)) {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Duplicate config salt",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final salts = frostInfo.knownSalts; // Fixed length list.
|
||||
final newSalts = List<String>.from(salts)..add(salt);
|
||||
final mainDB = ref.read(mainDBProvider);
|
||||
await mainDB.isar.writeTxn(() async {
|
||||
final info = frostInfo;
|
||||
await mainDB.isar.frostWalletInfo.delete(info.id);
|
||||
await mainDB.isar.frostWalletInfo.put(
|
||||
info.copyWith(knownSalts: newSalts),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
ref.read(pFrostResharingData).myName = frostInfo.myName;
|
||||
ref.read(pFrostResharingData).resharerConfig = config;
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
DisplayReshareConfigView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
String _validateInputData() {
|
||||
final threshold = int.tryParse(_newThresholdController.text);
|
||||
if (threshold == null) {
|
||||
return "Choose a threshold";
|
||||
}
|
||||
|
||||
final partsCount = int.tryParse(_newParticipantsCountController.text);
|
||||
if (partsCount == null) {
|
||||
return "Choose total number of participants";
|
||||
}
|
||||
|
||||
if (threshold > partsCount) {
|
||||
return "Threshold cannot be greater than the number of participants";
|
||||
}
|
||||
|
||||
if (partsCount < 2) {
|
||||
return "At least two participants required";
|
||||
}
|
||||
|
||||
if (controllers.length != partsCount) {
|
||||
return "Participants count error";
|
||||
}
|
||||
|
||||
final hasEmptyParticipants = controllers
|
||||
.map((e) => e.text.isEmpty)
|
||||
.reduce((value, element) => value |= element);
|
||||
if (hasEmptyParticipants) {
|
||||
return "Participants must not be empty";
|
||||
}
|
||||
|
||||
if (controllers.length != controllers.map((e) => e.text).toSet().length) {
|
||||
return "Duplicate participant name found";
|
||||
}
|
||||
|
||||
return "valid";
|
||||
}
|
||||
|
||||
void _participantsCountChanged(String newValue) {
|
||||
final count = int.tryParse(newValue);
|
||||
if (count != null) {
|
||||
if (count > _participantsCount) {
|
||||
for (int i = _participantsCount; i < count; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
}
|
||||
|
||||
_participantsCount = count;
|
||||
setState(() {});
|
||||
} else if (count < _participantsCount) {
|
||||
for (int i = _participantsCount; i > count; i--) {
|
||||
final last = controllers.removeLast();
|
||||
last.dispose();
|
||||
}
|
||||
|
||||
_participantsCount = count;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_newThresholdController.dispose();
|
||||
_newParticipantsCountController.dispose();
|
||||
for (final e in controllers) {
|
||||
e.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Modify Participants",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"New threshold",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
controller: _newThresholdController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Text(
|
||||
"Number of participants",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
controller: _newParticipantsCountController,
|
||||
onChanged: _participantsCountChanged,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
Text(
|
||||
"Participants",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
if (controllers.isNotEmpty)
|
||||
Column(
|
||||
children: [
|
||||
for (int i = 0; i < controllers.length; i++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 10,
|
||||
),
|
||||
child: TextField(
|
||||
controller: controllers[i],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Generate config",
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
await _onPressed();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class DisplayReshareConfigView extends ConsumerStatefulWidget {
|
||||
const DisplayReshareConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/displayReshareConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<DisplayReshareConfigView> createState() =>
|
||||
_DisplayReshareConfigViewState();
|
||||
}
|
||||
|
||||
class _DisplayReshareConfigViewState
|
||||
extends ConsumerState<DisplayReshareConfigView> {
|
||||
late final bool iAmInvolved;
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
|
||||
final serializedKeys = await wallet.getSerializedKeys();
|
||||
if (mounted) {
|
||||
final result = Frost.beginResharer(
|
||||
serializedKeys: serializedKeys!,
|
||||
config: ref.read(pFrostResharingData).resharerConfig!,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).startResharerData = result;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
BeginResharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
|
||||
final myOldIndex = frostInfo.participants.indexOf(frostInfo.myName);
|
||||
|
||||
iAmInvolved = ref
|
||||
.read(pFrostResharingData)
|
||||
.configData!
|
||||
.resharers
|
||||
.contains(myOldIndex);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Resharer config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||
size: 220,
|
||||
backgroundColor:
|
||||
Theme.of(context).extension<StackColors>()!.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Config",
|
||||
detail: ref.watch(pFrostResharingData).resharerConfig!,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: ref.watch(pFrostResharingData).resharerConfig!,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: Util.isDesktop ? 64 : 16,
|
||||
),
|
||||
if (!Util.isDesktop)
|
||||
const Spacer(
|
||||
flex: 2,
|
||||
),
|
||||
if (iAmInvolved)
|
||||
PrimaryButton(
|
||||
label: "Start resharing",
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:frostdart/frostdart.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/format.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class ImportReshareConfigView extends ConsumerStatefulWidget {
|
||||
const ImportReshareConfigView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/importReshareConfigView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<ImportReshareConfigView> createState() =>
|
||||
_ImportReshareConfigViewState();
|
||||
}
|
||||
|
||||
class _ImportReshareConfigViewState
|
||||
extends ConsumerState<ImportReshareConfigView> {
|
||||
late final TextEditingController configFieldController;
|
||||
late final FocusNode configFocusNode;
|
||||
|
||||
bool _configEmpty = true;
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
|
||||
ref.read(pFrostResharingData).reset();
|
||||
ref.read(pFrostResharingData).myName = frostInfo.myName;
|
||||
ref.read(pFrostResharingData).resharerConfig = configFieldController.text;
|
||||
|
||||
String? salt;
|
||||
try {
|
||||
salt = Format.uint8listToString(
|
||||
resharerSalt(
|
||||
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
throw Exception("Bad resharer config");
|
||||
}
|
||||
|
||||
if (frostInfo.knownSalts.contains(salt)) {
|
||||
throw Exception("Duplicate config salt");
|
||||
} else {
|
||||
final salts = frostInfo.knownSalts;
|
||||
salts.add(salt);
|
||||
final mainDB = ref.read(mainDBProvider);
|
||||
await mainDB.isar.writeTxn(() async {
|
||||
final info = frostInfo;
|
||||
await mainDB.isar.frostWalletInfo.delete(info.id);
|
||||
await mainDB.isar.frostWalletInfo.put(
|
||||
info.copyWith(knownSalts: salts),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final serializedKeys = await ref.read(secureStoreProvider).read(
|
||||
key: "{${widget.walletId}}_serializedFROSTKeys",
|
||||
);
|
||||
if (mounted) {
|
||||
final result = Frost.beginResharer(
|
||||
serializedKeys: serializedKeys!,
|
||||
config: ref.read(pFrostResharingData).resharerConfig!,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).startResharerData = result;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
BeginResharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
configFieldController = TextEditingController();
|
||||
configFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
configFieldController.dispose();
|
||||
configFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: const DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
),
|
||||
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(
|
||||
"Import FROST reshare config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frConfigTextFieldKey"),
|
||||
controller: configFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: configFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter config",
|
||||
configFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _configEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_configEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frConfigClearButtonKey"),
|
||||
onTap: () {
|
||||
configFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_configEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||
key: const Key("frConfigPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
configFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _configEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_configEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start resharing",
|
||||
enabled: !_configEmpty,
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
await _onPressed();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,439 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class BeginResharingView extends ConsumerStatefulWidget {
|
||||
const BeginResharingView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/beginResharingView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<BeginResharingView> createState() => _BeginResharingViewState();
|
||||
}
|
||||
|
||||
class _BeginResharingViewState extends ConsumerState<BeginResharingView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<int> resharerIndexes;
|
||||
late final int myResharerIndexIndex;
|
||||
late final String myResharerStart;
|
||||
late final bool amOutgoingParticipant;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
if (!amOutgoingParticipant) {
|
||||
// collect resharer strings
|
||||
final resharerStarts = controllers.map((e) => e.text).toList();
|
||||
if (myResharerIndexIndex >= 0) {
|
||||
// only insert my own at the correct index if I am a resharer
|
||||
resharerStarts.insert(myResharerIndexIndex, myResharerStart);
|
||||
}
|
||||
|
||||
final result = Frost.beginReshared(
|
||||
myName: ref.read(pFrostResharingData).myName!,
|
||||
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||
resharerStarts: resharerStarts,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).startResharedData = result;
|
||||
}
|
||||
await Navigator.of(context).pushNamed(
|
||||
ContinueResharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// TODO: optimize this by creating watcher providers (similar to normal WalletInfo)
|
||||
final frostInfo = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
.frostWalletInfo
|
||||
.getByWalletIdSync(widget.walletId)!;
|
||||
final myOldIndex =
|
||||
frostInfo.participants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||
|
||||
myResharerStart =
|
||||
ref.read(pFrostResharingData).startResharerData!.resharerStart;
|
||||
|
||||
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||
myResharerIndexIndex = resharerIndexes.indexOf(myOldIndex);
|
||||
if (myResharerIndexIndex >= 0) {
|
||||
// remove my name for now as we don't need a text field for it
|
||||
resharerIndexes.removeAt(myResharerIndexIndex);
|
||||
}
|
||||
|
||||
amOutgoingParticipant = !ref
|
||||
.read(pFrostResharingData)
|
||||
.configData!
|
||||
.newParticipants
|
||||
.contains(ref.read(pFrostResharingData).myName!);
|
||||
|
||||
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: Util.isDesktop
|
||||
? DesktopWalletView.routeName
|
||||
: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: DesktopWalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Resharers",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myResharerStart,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My resharer",
|
||||
detail: myResharerStart,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myResharerStart,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myResharerStart,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < resharerIndexes.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostResharerTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter index "
|
||||
"${resharerIndexes[i]}"
|
||||
"'s resharer",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Resharer Field Input.",
|
||||
key: Key(
|
||||
"frostResharerClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
|
||||
key: Key(
|
||||
"frostResharerPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel: "Scan QR Button. "
|
||||
"Opens Camera For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostCommitmentsScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions "
|
||||
"while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: amOutgoingParticipant ||
|
||||
!fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,429 @@
|
|||
import 'dart:ffi';
|
||||
|
||||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.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/custom_buttons/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
class ContinueResharingView extends ConsumerStatefulWidget {
|
||||
const ContinueResharingView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/continueResharingView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<ContinueResharingView> createState() =>
|
||||
_ContinueResharingViewState();
|
||||
}
|
||||
|
||||
class _ContinueResharingViewState extends ConsumerState<ContinueResharingView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<String> newParticipants;
|
||||
late final int myIndex;
|
||||
late final String? myEncryptionKey;
|
||||
late final bool amOutgoingParticipant;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
bool _buttonLock = false;
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
// collect encryptionKeys strings and insert my own at the correct index
|
||||
final encryptionKeys = controllers.map((e) => e.text).toList();
|
||||
if (!amOutgoingParticipant) {
|
||||
encryptionKeys.insert(myIndex, myEncryptionKey!);
|
||||
}
|
||||
|
||||
final result = Frost.finishResharer(
|
||||
machine: ref.read(pFrostResharingData).startResharerData!.machine.ref,
|
||||
encryptionKeysOfResharedTo: encryptionKeys,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).resharerComplete = result;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
FinishResharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
myEncryptionKey =
|
||||
ref.read(pFrostResharingData).startResharedData?.resharedStart;
|
||||
|
||||
newParticipants = ref.read(pFrostResharingData).configData!.newParticipants;
|
||||
myIndex = newParticipants.indexOf(ref.read(pFrostResharingData).myName!);
|
||||
|
||||
if (myIndex >= 0) {
|
||||
// remove my name for now as we don't need a text field for it
|
||||
newParticipants.removeAt(myIndex);
|
||||
}
|
||||
|
||||
if (myEncryptionKey == null && myIndex == -1) {
|
||||
amOutgoingParticipant = true;
|
||||
} else if (myEncryptionKey != null && myIndex >= 0) {
|
||||
amOutgoingParticipant = false;
|
||||
} else {
|
||||
throw Exception("Invalid resharing state");
|
||||
}
|
||||
|
||||
for (int i = 0; i < newParticipants.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: Util.isDesktop
|
||||
? DesktopWalletView.routeName
|
||||
: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: DesktopWalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: WalletView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Encryption keys",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!amOutgoingParticipant)
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: myEncryptionKey!,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!amOutgoingParticipant) const _Div(),
|
||||
if (!amOutgoingParticipant)
|
||||
DetailItem(
|
||||
title: "My encryption key",
|
||||
detail: myEncryptionKey!,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: myEncryptionKey!,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: myEncryptionKey!,
|
||||
),
|
||||
),
|
||||
if (!amOutgoingParticipant) const _Div(),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < newParticipants.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostEncryptionKeyTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter "
|
||||
"${newParticipants[i]}"
|
||||
"'s encryption key",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Encryption Key Field Input.",
|
||||
key: Key(
|
||||
"frostEncryptionKeyClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Encryption Key Field Input.",
|
||||
key: Key(
|
||||
"frostEncryptionKeyPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel: "Scan QR Button. "
|
||||
"Opens Camera For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostCommitmentsScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions "
|
||||
"while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/custom_buttons/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class NewContinueSharingView extends ConsumerStatefulWidget {
|
||||
const NewContinueSharingView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/NewContinueSharingView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<NewContinueSharingView> createState() =>
|
||||
_NewContinueSharingViewState();
|
||||
}
|
||||
|
||||
class _NewContinueSharingViewState
|
||||
extends ConsumerState<NewContinueSharingView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName:
|
||||
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: HomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Encryption keys",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 220,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QrImageView(
|
||||
data: ref
|
||||
.watch(pFrostResharingData)
|
||||
.startResharedData!
|
||||
.resharedStart,
|
||||
size: 220,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.background,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "My encryption key",
|
||||
detail: ref
|
||||
.watch(pFrostResharingData)
|
||||
.startResharedData!
|
||||
.resharedStart,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: ref
|
||||
.watch(pFrostResharingData)
|
||||
.startResharedData!
|
||||
.resharedStart,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: ref
|
||||
.watch(pFrostResharingData)
|
||||
.startResharedData!
|
||||
.resharedStart,
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Continue",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
FinishResharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,431 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.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/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
||||
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.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/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class NewImportResharerConfigView extends ConsumerStatefulWidget {
|
||||
const NewImportResharerConfigView({
|
||||
super.key,
|
||||
required this.walletName,
|
||||
required this.coin,
|
||||
});
|
||||
|
||||
static const String routeName = "/newImportResharerConfigView";
|
||||
|
||||
final String walletName;
|
||||
final Coin coin;
|
||||
|
||||
@override
|
||||
ConsumerState<NewImportResharerConfigView> createState() =>
|
||||
_NewImportResharerConfigViewState();
|
||||
}
|
||||
|
||||
class _NewImportResharerConfigViewState
|
||||
extends ConsumerState<NewImportResharerConfigView> {
|
||||
late final TextEditingController myNameFieldController, configFieldController;
|
||||
late final FocusNode myNameFocusNode, configFocusNode;
|
||||
|
||||
bool _nameEmpty = true, _configEmpty = true;
|
||||
|
||||
bool _buttonLock = false;
|
||||
|
||||
Future<IncompleteFrostWallet> _createWallet() async {
|
||||
final info = WalletInfo.createNew(
|
||||
name: widget.walletName,
|
||||
coin: widget.coin,
|
||||
);
|
||||
|
||||
final wallet = IncompleteFrostWallet();
|
||||
wallet.info = info;
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
myNameFieldController = TextEditingController();
|
||||
configFieldController = TextEditingController();
|
||||
myNameFocusNode = FocusNode();
|
||||
configFocusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
myNameFieldController.dispose();
|
||||
configFieldController.dispose();
|
||||
myNameFocusNode.dispose();
|
||||
configFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Import FROST reshare config",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frMyNameTextFieldKey"),
|
||||
controller: myNameFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_nameEmpty = myNameFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: myNameFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"My name",
|
||||
myNameFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _nameEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_nameEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frMyNameClearButtonKey"),
|
||||
onTap: () {
|
||||
myNameFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_nameEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Name Field.",
|
||||
key: const Key("frMyNamePasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
myNameFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_nameEmpty =
|
||||
myNameFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _nameEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: const Key("frConfigTextFieldKey"),
|
||||
controller: configFieldController,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
_configEmpty = configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
focusNode: configFocusNode,
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
decoration: standardInputDecoration(
|
||||
"Enter config",
|
||||
configFocusNode,
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: _configEmpty
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!_configEmpty
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Config Field.",
|
||||
key: const Key("frConfigClearButtonKey"),
|
||||
onTap: () {
|
||||
configFieldController.text = "";
|
||||
|
||||
setState(() {
|
||||
_configEmpty = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Config Field Input.",
|
||||
key: const Key("frConfigPasteButtonKey"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
configFieldController.text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
},
|
||||
child: _configEmpty
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (_configEmpty)
|
||||
TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Scan QR Button. Opens Camera For Scanning QR Code.",
|
||||
key: const Key("frConfigScanQrButtonKey"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult = await BarcodeScanner.scan();
|
||||
|
||||
configFieldController.text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
_configEmpty =
|
||||
configFieldController.text.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Start",
|
||||
enabled: !_nameEmpty && !_configEmpty,
|
||||
onPressed: () async {
|
||||
if (FocusScope.of(context).hasFocus) {
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
ref.read(pFrostResharingData).reset();
|
||||
ref.read(pFrostResharingData).myName =
|
||||
myNameFieldController.text;
|
||||
ref.read(pFrostResharingData).resharerConfig =
|
||||
configFieldController.text;
|
||||
|
||||
if (!ref
|
||||
.read(pFrostResharingData)
|
||||
.configData!
|
||||
.newParticipants
|
||||
.contains(ref.read(pFrostResharingData).myName!)) {
|
||||
ref.read(pFrostResharingData).reset();
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "My name not found in config participants",
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Exception? ex;
|
||||
final wallet = await showLoading(
|
||||
whileFuture: _createWallet(),
|
||||
context: context,
|
||||
message: "Setting up wallet",
|
||||
isDesktop: Util.isDesktop,
|
||||
onException: (e) => ex = e,
|
||||
);
|
||||
|
||||
if (ex != null) {
|
||||
throw ex!;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ref.read(pFrostResharingData).incompleteWallet = wallet!;
|
||||
await Navigator.of(context).pushNamed(
|
||||
NewStartResharingView.routeName,
|
||||
arguments: wallet.walletId,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,373 @@
|
|||
import 'package:barcode_scan2/barcode_scan2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/logger.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/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_text_field.dart';
|
||||
import 'package:stackwallet/widgets/textfield_icon_button.dart';
|
||||
|
||||
import 'package:stackwallet/pages/frost_mascot.dart';
|
||||
|
||||
class NewStartResharingView extends ConsumerStatefulWidget {
|
||||
const NewStartResharingView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/newStartResharingView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<NewStartResharingView> createState() =>
|
||||
_NewStartResharingViewState();
|
||||
}
|
||||
|
||||
class _NewStartResharingViewState extends ConsumerState<NewStartResharingView> {
|
||||
final List<TextEditingController> controllers = [];
|
||||
final List<FocusNode> focusNodes = [];
|
||||
|
||||
late final List<int> resharerIndexes;
|
||||
|
||||
final List<bool> fieldIsEmptyFlags = [];
|
||||
|
||||
bool _buttonLock = false;
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
// collect resharer strings
|
||||
final resharerStarts = controllers.map((e) => e.text).toList();
|
||||
|
||||
final result = Frost.beginReshared(
|
||||
myName: ref.read(pFrostResharingData).myName!,
|
||||
resharerConfig: ref.read(pFrostResharingData).resharerConfig!,
|
||||
resharerStarts: resharerStarts,
|
||||
);
|
||||
|
||||
ref.read(pFrostResharingData).startResharedData = result;
|
||||
|
||||
await Navigator.of(context).pushNamed(
|
||||
NewContinueSharingView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
resharerIndexes = ref.read(pFrostResharingData).configData!.resharers;
|
||||
|
||||
for (int i = 0; i < resharerIndexes.length; i++) {
|
||||
controllers.add(TextEditingController());
|
||||
focusNodes.add(FocusNode());
|
||||
fieldIsEmptyFlags.add(true);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (int i = 0; i < controllers.length; i++) {
|
||||
controllers[i].dispose();
|
||||
}
|
||||
for (int i = 0; i < focusNodes.length; i++) {
|
||||
focusNodes[i].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName:
|
||||
Util.isDesktop ? DesktopHomeView.routeName : HomeView.routeName,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: DesktopHomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: FrostMascot(
|
||||
title: 'Lorem ipsum',
|
||||
body: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam est justo, ',
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: HomeView.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
"Resharers",
|
||||
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.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < resharerIndexes.length; i++)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius,
|
||||
),
|
||||
child: TextField(
|
||||
key: Key("frostResharerTextFieldKey_$i"),
|
||||
controller: controllers[i],
|
||||
focusNode: focusNodes[i],
|
||||
readOnly: false,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
style: STextStyles.field(context),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i].text.isEmpty;
|
||||
});
|
||||
},
|
||||
decoration: standardInputDecoration(
|
||||
"Enter index "
|
||||
"${resharerIndexes[i]}"
|
||||
"'s resharer",
|
||||
focusNodes[i],
|
||||
context,
|
||||
).copyWith(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
top: 6,
|
||||
bottom: 8,
|
||||
right: 5,
|
||||
),
|
||||
suffixIcon: Padding(
|
||||
padding: fieldIsEmptyFlags[i]
|
||||
? const EdgeInsets.only(right: 8)
|
||||
: const EdgeInsets.only(right: 0),
|
||||
child: UnconstrainedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
!fieldIsEmptyFlags[i]
|
||||
? TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Clear Button. Clears The Resharer Field Input.",
|
||||
key: Key(
|
||||
"frostResharerClearButtonKey_$i"),
|
||||
onTap: () {
|
||||
controllers[i].text = "";
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] = true;
|
||||
});
|
||||
},
|
||||
child: const XIcon(),
|
||||
)
|
||||
: TextFieldIconButton(
|
||||
semanticsLabel:
|
||||
"Paste Button. Pastes From Clipboard To Resharer Field Input.",
|
||||
key: Key(
|
||||
"frostResharerPasteButtonKey_$i"),
|
||||
onTap: () async {
|
||||
final ClipboardData? data =
|
||||
await Clipboard.getData(
|
||||
Clipboard.kTextPlain);
|
||||
if (data?.text != null &&
|
||||
data!.text!.isNotEmpty) {
|
||||
controllers[i].text =
|
||||
data.text!.trim();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
},
|
||||
child: fieldIsEmptyFlags[i]
|
||||
? const ClipboardIcon()
|
||||
: const XIcon(),
|
||||
),
|
||||
if (fieldIsEmptyFlags[i])
|
||||
TextFieldIconButton(
|
||||
semanticsLabel: "Scan QR Button. "
|
||||
"Opens Camera For Scanning QR Code.",
|
||||
key: Key(
|
||||
"frostCommitmentsScanQrButtonKey_$i"),
|
||||
onTap: () async {
|
||||
try {
|
||||
if (FocusScope.of(context)
|
||||
.hasFocus) {
|
||||
FocusScope.of(context)
|
||||
.unfocus();
|
||||
await Future<void>.delayed(
|
||||
const Duration(
|
||||
milliseconds: 75));
|
||||
}
|
||||
|
||||
final qrResult =
|
||||
await BarcodeScanner.scan();
|
||||
|
||||
controllers[i].text =
|
||||
qrResult.rawContent;
|
||||
|
||||
setState(() {
|
||||
fieldIsEmptyFlags[i] =
|
||||
controllers[i]
|
||||
.text
|
||||
.isEmpty;
|
||||
});
|
||||
} on PlatformException catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to get camera permissions "
|
||||
"while trying to scan qr code: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const QrCodeIcon(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Continue",
|
||||
enabled: !fieldIsEmptyFlags.reduce((v, e) => v |= e),
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/home_view/home_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart';
|
||||
import 'package:stackwallet/providers/db/main_db_provider.dart';
|
||||
import 'package:stackwallet/providers/frost_wallet/frost_wallet_providers.dart';
|
||||
import 'package:stackwallet/providers/global/node_service_provider.dart';
|
||||
import 'package:stackwallet/providers/global/prefs_provider.dart';
|
||||
import 'package:stackwallet/providers/global/secure_store_provider.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.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/simple_copy_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/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/frost_interruption_dialog.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class VerifyUpdatedWalletView extends ConsumerStatefulWidget {
|
||||
const VerifyUpdatedWalletView({
|
||||
super.key,
|
||||
required this.walletId,
|
||||
});
|
||||
|
||||
static const String routeName = "/verifyUpdatedWalletView";
|
||||
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
ConsumerState<VerifyUpdatedWalletView> createState() =>
|
||||
_VerifyUpdatedWalletViewState();
|
||||
}
|
||||
|
||||
class _VerifyUpdatedWalletViewState
|
||||
extends ConsumerState<VerifyUpdatedWalletView> {
|
||||
late final String config;
|
||||
late final String serializedKeys;
|
||||
late final String reshareId;
|
||||
|
||||
late final bool isNew;
|
||||
|
||||
bool _buttonLock = false;
|
||||
Future<void> _onPressed() async {
|
||||
if (_buttonLock) {
|
||||
return;
|
||||
}
|
||||
_buttonLock = true;
|
||||
|
||||
try {
|
||||
Exception? ex;
|
||||
|
||||
final BitcoinFrostWallet wallet;
|
||||
|
||||
if (isNew) {
|
||||
wallet = await ref
|
||||
.read(pFrostResharingData)
|
||||
.incompleteWallet!
|
||||
.toBitcoinFrostWallet(
|
||||
mainDB: ref.read(mainDBProvider),
|
||||
secureStorageInterface: ref.read(secureStoreProvider),
|
||||
nodeService: ref.read(nodeServiceChangeNotifierProvider),
|
||||
prefs: ref.read(prefsChangeNotifierProvider),
|
||||
);
|
||||
|
||||
await wallet.info.setMnemonicVerified(
|
||||
isar: ref.read(mainDBProvider).isar,
|
||||
);
|
||||
|
||||
ref.read(pWallets).addWallet(wallet);
|
||||
} else {
|
||||
wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId) as BitcoinFrostWallet;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
await showLoading(
|
||||
whileFuture: wallet.updateWithResharedData(
|
||||
serializedKeys: serializedKeys,
|
||||
multisigConfig: config,
|
||||
isNewWallet: isNew,
|
||||
),
|
||||
context: context,
|
||||
message: isNew ? "Creating wallet" : "Updating wallet data",
|
||||
isDesktop: Util.isDesktop,
|
||||
onException: (e) => ex = e,
|
||||
);
|
||||
|
||||
if (ex != null) {
|
||||
throw ex!;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ref.read(pFrostResharingData).reset();
|
||||
|
||||
Navigator.of(context).popUntil(
|
||||
ModalRoute.withName(
|
||||
_popUntilPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"$e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
if (mounted) {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => StackOkDialog(
|
||||
title: "Error",
|
||||
message: e.toString(),
|
||||
desktopPopRootNavigator: Util.isDesktop,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_buttonLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
String get _popUntilPath => isNew
|
||||
? Util.isDesktop
|
||||
? DesktopHomeView.routeName
|
||||
: HomeView.routeName
|
||||
: Util.isDesktop
|
||||
? DesktopWalletView.routeName
|
||||
: WalletView.routeName;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
config = ref.read(pFrostResharingData).newWalletData!.multisigConfig;
|
||||
serializedKeys =
|
||||
ref.read(pFrostResharingData).newWalletData!.serializedKeys;
|
||||
reshareId = ref.read(pFrostResharingData).newWalletData!.resharedId;
|
||||
|
||||
isNew = ref.read(pFrostResharingData).incompleteWallet != null &&
|
||||
ref.read(pFrostResharingData).incompleteWallet!.walletId ==
|
||||
widget.walletId;
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: _popUntilPath,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
},
|
||||
child: ConditionalParent(
|
||||
condition: Util.isDesktop,
|
||||
builder: (child) => DesktopScaffold(
|
||||
background: Theme.of(context).extension<StackColors>()!.background,
|
||||
appBar: DesktopAppBar(
|
||||
isCompactHeight: false,
|
||||
leading: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: _popUntilPath,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: ExitToMyStackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: _popUntilPath,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: AppBarBackButton(
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => FrostInterruptionDialog(
|
||||
type: FrostInterruptionDialogType.resharing,
|
||||
popUntilOnYesRouteName: _popUntilPath,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Ensure your reshare ID matches that of each other participant",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "ID",
|
||||
detail: reshareId,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: reshareId,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: reshareId,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
const _Div(),
|
||||
Text(
|
||||
"Back up your keys and config",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "Config",
|
||||
detail: config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: config,
|
||||
),
|
||||
),
|
||||
const _Div(),
|
||||
DetailItem(
|
||||
title: "Keys",
|
||||
detail: serializedKeys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: serializedKeys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: serializedKeys,
|
||||
),
|
||||
),
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
const _Div(),
|
||||
PrimaryButton(
|
||||
label: "Confirm",
|
||||
onPressed: _onPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Div extends StatelessWidget {
|
||||
const _Div({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox(
|
||||
height: 12,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,15 +17,20 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/address_utils.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/clipboard_interface.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart';
|
||||
import 'package:stackwallet/widgets/detail_item.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class WalletBackupView extends ConsumerWidget {
|
||||
|
@ -33,6 +38,7 @@ class WalletBackupView extends ConsumerWidget {
|
|||
Key? key,
|
||||
required this.walletId,
|
||||
required this.mnemonic,
|
||||
this.frostWalletData,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -40,11 +46,21 @@ class WalletBackupView extends ConsumerWidget {
|
|||
|
||||
final String walletId;
|
||||
final List<String> mnemonic;
|
||||
final ({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
({String config, String keys})? prevGen,
|
||||
})? frostWalletData;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
|
||||
final bool frost = frostWalletData != null;
|
||||
final prevGen = frostWalletData?.prevGen != null;
|
||||
|
||||
return Background(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
|
||||
|
@ -91,139 +107,261 @@ class WalletBackupView extends ConsumerWidget {
|
|||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
ref.watch(pWalletName(walletId)),
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.label(context).copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"Recovery Phrase",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MnemonicTable(
|
||||
words: mnemonic,
|
||||
isDesktop: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context),
|
||||
onPressed: () {
|
||||
String data = AddressUtils.encodeQRSeedData(mnemonic);
|
||||
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
final width = MediaQuery.of(context).size.width / 2;
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Recovery phrase QR code",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
// key: _qrKey,
|
||||
child: SizedBox(
|
||||
width: width + 20,
|
||||
height: width + 20,
|
||||
child: QrImageView(
|
||||
data: data,
|
||||
size: width,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.popupBG,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
child: frost
|
||||
? LayoutBuilder(
|
||||
builder: (builderContext, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 24,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Please write down your backup data. Keep it safe and "
|
||||
"never share it with anyone. "
|
||||
"Your backup data is the only way you can access your "
|
||||
"funds if you forget your PIN, lose your phone, etc."
|
||||
"\n\n"
|
||||
"Stack Wallet does not keep nor is able to restore "
|
||||
"your backup data. "
|
||||
"Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
// await _capturePng(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(context),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
// DetailItem(
|
||||
// title: "My name",
|
||||
// detail: frostWalletData!.myName,
|
||||
// button: Util.isDesktop
|
||||
// ? IconCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// )
|
||||
// : SimpleCopyButton(
|
||||
// data: frostWalletData!.myName,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(
|
||||
// height: 16,
|
||||
// ),
|
||||
DetailItem(
|
||||
title: "Multisig config",
|
||||
detail: frostWalletData!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.config,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
DetailItem(
|
||||
title: "Keys",
|
||||
detail: frostWalletData!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.keys,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
if (prevGen)
|
||||
RoundedWhiteContainer(
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: STextStyles.button(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
"Previous generation info",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous multisig config",
|
||||
detail: frostWalletData!.prevGen!.config,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data:
|
||||
frostWalletData!.prevGen!.config,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data:
|
||||
frostWalletData!.prevGen!.config,
|
||||
),
|
||||
),
|
||||
if (prevGen)
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
if (prevGen)
|
||||
DetailItem(
|
||||
title: "Previous keys",
|
||||
detail: frostWalletData!.prevGen!.keys,
|
||||
button: Util.isDesktop
|
||||
? IconCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
)
|
||||
: SimpleCopyButton(
|
||||
data: frostWalletData!.prevGen!.keys,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show QR Code",
|
||||
style: STextStyles.button(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
ref.watch(pWalletName(walletId)),
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.label(context).copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
"Recovery Phrase",
|
||||
textAlign: TextAlign.center,
|
||||
style: STextStyles.pageTitleH1(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).extension<StackColors>()!.popupBG,
|
||||
borderRadius: BorderRadius.circular(
|
||||
Constants.size.circularBorderRadius),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
|
||||
style: STextStyles.label(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MnemonicTable(
|
||||
words: mnemonic,
|
||||
isDesktop: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
TextButton(
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getPrimaryEnabledButtonStyle(context),
|
||||
onPressed: () {
|
||||
String data = AddressUtils.encodeQRSeedData(mnemonic);
|
||||
|
||||
showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (_) {
|
||||
final width = MediaQuery.of(context).size.width / 2;
|
||||
return StackDialogBase(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
"Recovery phrase QR code",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
// key: _qrKey,
|
||||
child: SizedBox(
|
||||
width: width + 20,
|
||||
height: width + 20,
|
||||
child: QrImageView(
|
||||
data: data,
|
||||
size: width,
|
||||
backgroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.popupBG,
|
||||
foregroundColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
// await _capturePng(true);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
style: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.getSecondaryEnabledButtonStyle(
|
||||
context),
|
||||
child: Text(
|
||||
"Cancel",
|
||||
style: STextStyles.button(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.accentColorDark),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show QR Code",
|
||||
style: STextStyles.button(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
|
|||
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_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_wallet_settings/change_representative_view.dart';
|
||||
|
@ -39,6 +40,7 @@ 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/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
import 'package:stackwallet/widgets/background.dart';
|
||||
|
@ -193,6 +195,21 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
children: [
|
||||
if (coin == Coin.bitcoinFrost ||
|
||||
coin == Coin.bitcoinFrostTestNet)
|
||||
if (coin == Coin.bitcoinFrost ||
|
||||
coin == Coin.bitcoinFrostTestNet)
|
||||
SettingsListButton(
|
||||
iconAssetName: Assets.svg.addressBook2,
|
||||
iconSize: 16,
|
||||
title: "FROST Multisig settings",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
FrostMSWalletOptionsView.routeName,
|
||||
arguments: walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
SettingsListButton(
|
||||
iconAssetName: Assets.svg.addressBook,
|
||||
iconSize: 16,
|
||||
|
@ -235,39 +252,83 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
|
|||
final wallet = ref
|
||||
.read(pWallets)
|
||||
.getWallet(widget.walletId);
|
||||
// TODO: [prio=frost] take wallets that don't have a mnemonic into account
|
||||
if (wallet is MnemonicInterface) {
|
||||
final mnemonic =
|
||||
await wallet.getMnemonicAsWords();
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute:
|
||||
RouteGenerator
|
||||
.useMaterialPageRoute,
|
||||
builder: (_) => LockscreenView(
|
||||
routeOnSuccessArguments:
|
||||
Tuple2(
|
||||
walletId, mnemonic),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
WalletBackupView
|
||||
.routeName,
|
||||
biometricsCancelButtonString:
|
||||
"CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to view recovery phrase",
|
||||
biometricsAuthenticationTitle:
|
||||
"View recovery phrase",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name:
|
||||
"/viewRecoverPhraseLockscreen"),
|
||||
),
|
||||
// TODO: [prio=med] take wallets that don't have a mnemonic into account
|
||||
|
||||
List<String>? mnemonic;
|
||||
({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
({
|
||||
String config,
|
||||
String keys
|
||||
})? prevGen,
|
||||
})? frostWalletData;
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
List<Future<dynamic>> futures = [];
|
||||
|
||||
futures.addAll(
|
||||
[
|
||||
wallet.getSerializedKeys(),
|
||||
wallet.getMultisigConfig(),
|
||||
wallet.getSerializedKeysPrevGen(),
|
||||
wallet.getMultisigConfigPrevGen(),
|
||||
],
|
||||
);
|
||||
|
||||
final results =
|
||||
await Future.wait(futures);
|
||||
|
||||
if (results.length == 5) {
|
||||
frostWalletData = (
|
||||
myName: wallet.frostInfo.myName,
|
||||
config: results[1],
|
||||
keys: results[0],
|
||||
prevGen: results[2] == null ||
|
||||
results[3] == null
|
||||
? null
|
||||
: (
|
||||
config: results[3],
|
||||
keys: results[2],
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (wallet
|
||||
is MnemonicInterface) {
|
||||
mnemonic =
|
||||
await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.push(
|
||||
context,
|
||||
RouteGenerator.getRoute(
|
||||
shouldUseMaterialRoute:
|
||||
RouteGenerator
|
||||
.useMaterialPageRoute,
|
||||
builder: (_) => LockscreenView(
|
||||
routeOnSuccessArguments: (
|
||||
walletId: walletId,
|
||||
mnemonic: mnemonic ?? [],
|
||||
frostWalletData:
|
||||
frostWalletData,
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess:
|
||||
WalletBackupView.routeName,
|
||||
biometricsCancelButtonString:
|
||||
"CANCEL",
|
||||
biometricsLocalizedReason:
|
||||
"Authenticate to view recovery phrase",
|
||||
biometricsAuthenticationTitle:
|
||||
"View recovery phrase",
|
||||
),
|
||||
settings: const RouteSettings(
|
||||
name:
|
||||
"/viewRecoverPhraseLockscreen"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -23,7 +23,6 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.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:tuple/tuple.dart';
|
||||
|
||||
enum FiroRescanRecoveryErrorViewOption {
|
||||
retry,
|
||||
|
@ -209,7 +208,7 @@ class _FiroRescanRecoveryErrorViewState
|
|||
children: [
|
||||
if (!Util.isDesktop) const Spacer(),
|
||||
Text(
|
||||
"Failed to rescan firo wallet",
|
||||
"Failed to rescan Firo wallet",
|
||||
style: STextStyles.pageTitleH2(context),
|
||||
),
|
||||
Util.isDesktop
|
||||
|
@ -269,8 +268,10 @@ class _FiroRescanRecoveryErrorViewState
|
|||
shouldUseMaterialRoute:
|
||||
RouteGenerator.useMaterialPageRoute,
|
||||
builder: (_) => LockscreenView(
|
||||
routeOnSuccessArguments:
|
||||
Tuple2(widget.walletId, mnemonic),
|
||||
routeOnSuccessArguments: (
|
||||
walletId: widget.walletId,
|
||||
mnemonic: mnemonic,
|
||||
),
|
||||
showBackButton: true,
|
||||
routeOnSuccess: WalletBackupView.routeName,
|
||||
biometricsCancelButtonString: "CANCEL",
|
||||
|
|
|
@ -178,7 +178,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceSecondary != null)
|
||||
BalanceSelector(
|
||||
title: "Available lelantus balance",
|
||||
title: "Available Lelantus balance",
|
||||
coin: coin,
|
||||
balance: balanceSecondary.spendable,
|
||||
onPressed: () {
|
||||
|
@ -204,7 +204,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceSecondary != null)
|
||||
BalanceSelector(
|
||||
title: "Full lelantus balance",
|
||||
title: "Full Lelantus balance",
|
||||
coin: coin,
|
||||
balance: balanceSecondary.total,
|
||||
onPressed: () {
|
||||
|
@ -230,7 +230,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceTertiary != null)
|
||||
BalanceSelector(
|
||||
title: "Available spark balance",
|
||||
title: "Available Spark balance",
|
||||
coin: coin,
|
||||
balance: balanceTertiary.spendable,
|
||||
onPressed: () {
|
||||
|
@ -256,7 +256,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget {
|
|||
),
|
||||
if (balanceTertiary != null)
|
||||
BalanceSelector(
|
||||
title: "Full spark balance",
|
||||
title: "Full Spark balance",
|
||||
coin: coin,
|
||||
balance: balanceTertiary.total,
|
||||
onPressed: () {
|
||||
|
|
|
@ -909,7 +909,58 @@ class _TransactionV2DetailsViewState
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (coin == Coin.epicCash)
|
||||
RoundedWhiteContainer(
|
||||
padding: isDesktop
|
||||
? const EdgeInsets.all(16)
|
||||
: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"On chain note",
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
: STextStyles.itemSubtitle(
|
||||
context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
SelectableText(
|
||||
_transaction.onChainNote ?? "",
|
||||
style: isDesktop
|
||||
? STextStyles
|
||||
.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<
|
||||
StackColors>()!
|
||||
.textDark,
|
||||
)
|
||||
: STextStyles.itemSubtitle12(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isDesktop)
|
||||
IconCopyButton(
|
||||
data: _transaction.onChainNote ?? "",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDesktop
|
||||
? const _Divider()
|
||||
: const SizedBox(
|
||||
|
@ -996,7 +1047,9 @@ class _TransactionV2DetailsViewState
|
|||
.watch(
|
||||
pTransactionNote(
|
||||
(
|
||||
txid: _transaction.txid,
|
||||
txid: (coin == Coin.epicCash) ?
|
||||
_transaction.slateId.toString()
|
||||
: _transaction.txid,
|
||||
walletId: walletId
|
||||
),
|
||||
),
|
||||
|
|
|
@ -23,6 +23,7 @@ import 'package:stackwallet/providers/db/main_db_provider.dart';
|
|||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||
|
||||
|
@ -44,6 +45,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
|
|||
|
||||
late final StreamSubscription<List<TransactionV2>> _subscription;
|
||||
late final Query<TransactionV2> _query;
|
||||
late final Coin coin;
|
||||
|
||||
BorderRadius get _borderRadiusFirst {
|
||||
return BorderRadius.only(
|
||||
|
@ -69,6 +71,7 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
coin = ref.read(pWallets).getWallet(widget.walletId).info.coin;
|
||||
_query = ref
|
||||
.read(mainDBProvider)
|
||||
.isar
|
||||
|
@ -110,8 +113,6 @@ class _TransactionsV2ListState extends ConsumerState<TransactionsV2List> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final coin = ref.watch(pWallets).getWallet(widget.walletId).info.coin;
|
||||
|
||||
return FutureBuilder(
|
||||
future: _query.findAll(),
|
||||
builder: (fbContext, AsyncSnapshot<List<TransactionV2>> snapshot) {
|
||||
|
|
|
@ -29,6 +29,7 @@ import 'package:stackwallet/pages/ordinals/ordinals_view.dart';
|
|||
import 'package:stackwallet/pages/paynym/paynym_claim_view.dart';
|
||||
import 'package:stackwallet/pages/paynym/paynym_home_view.dart';
|
||||
import 'package:stackwallet/pages/receive_view/receive_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/send_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';
|
||||
|
@ -59,10 +60,12 @@ import 'package:stackwallet/utilities/clipboard_interface.dart';
|
|||
import 'package:stackwallet/utilities/constants.dart';
|
||||
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
||||
|
@ -305,6 +308,26 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
BackupFrequencyType.afterClosingAWallet) {
|
||||
unawaited(ref.read(autoSWBServiceProvider).doBackup());
|
||||
}
|
||||
|
||||
// Close the wallet according to syncing preferences.
|
||||
switch (ref.read(prefsChangeNotifierProvider).syncType) {
|
||||
case SyncingType.currentWalletOnly:
|
||||
// Close the wallet.
|
||||
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
case SyncingType.selectedWalletsAtStartup:
|
||||
// Close if this wallet is not in the list to be synced.
|
||||
if (!ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.walletIdsSyncOnStartup
|
||||
.contains(widget.walletId)) {
|
||||
unawaited(ref.watch(pWallets).getWallet(walletId).exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
}
|
||||
case SyncingType.allWalletsOnStartup:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildNetworkIcon(WalletSyncStatus status) {
|
||||
|
@ -973,10 +996,13 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
// break;
|
||||
// }
|
||||
Navigator.of(context).pushNamed(
|
||||
SendView.routeName,
|
||||
arguments: Tuple2(
|
||||
walletId,
|
||||
coin,
|
||||
ref.read(pWallets).getWallet(walletId)
|
||||
is BitcoinFrostWallet
|
||||
? FrostSendView.routeName
|
||||
: SendView.routeName,
|
||||
arguments: (
|
||||
walletId: walletId,
|
||||
coin: coin,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -198,6 +198,11 @@ class _FavoriteCardState extends ConsumerState<FavoriteCard> {
|
|||
pWalletBalanceSecondary(walletId),
|
||||
)
|
||||
.total;
|
||||
total += ref
|
||||
.watch(
|
||||
pWalletBalanceTertiary(walletId),
|
||||
)
|
||||
.total;
|
||||
}
|
||||
|
||||
Amount fiatTotal = Amount.zero;
|
||||
|
|
|
@ -61,7 +61,7 @@ class DesktopAddressCard extends ConsumerWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SelectableText(
|
||||
"${contactId == "default" ? entry.other! : entry.label} (${entry.coin.ticker})",
|
||||
"${contactId == "default" ? entry.other : entry.label} (${entry.coin.ticker})",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context).copyWith(
|
||||
color: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
),
|
||||
|
|
|
@ -294,6 +294,7 @@ class _BalanceDisplay extends ConsumerWidget {
|
|||
Amount total = ref.watch(pWalletBalance(walletId)).total;
|
||||
if (coin == Coin.firo || coin == Coin.firoTestNet) {
|
||||
total += ref.watch(pWalletBalanceSecondary(walletId)).total;
|
||||
total += ref.watch(pWalletBalanceTertiary(walletId)).total;
|
||||
}
|
||||
|
||||
return Text(
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:event_bus/event_bus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
@ -38,7 +38,9 @@ import 'package:stackwallet/themes/coin_icon_provider.dart';
|
|||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/assets.dart';
|
||||
import 'package:stackwallet/utilities/enums/backup_frequency_type.dart';
|
||||
import 'package:stackwallet/utilities/enums/sync_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/banano_wallet.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
|
@ -92,6 +94,26 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
unawaited(ref.read(autoSWBServiceProvider).doBackup());
|
||||
}
|
||||
|
||||
// Close the wallet according to syncing preferences.
|
||||
switch (ref.read(prefsChangeNotifierProvider).syncType) {
|
||||
case SyncingType.currentWalletOnly:
|
||||
// Close the wallet.
|
||||
unawaited(wallet.exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
case SyncingType.selectedWalletsAtStartup:
|
||||
// Close if this wallet is not in the list to be synced.
|
||||
if (!ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.walletIdsSyncOnStartup
|
||||
.contains(widget.walletId)) {
|
||||
unawaited(wallet.exit());
|
||||
// unawaited so we don't lag the UI.
|
||||
}
|
||||
case SyncingType.allWalletsOnStartup:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
|
||||
ref.read(currentWalletIdProvider.notifier).state = null;
|
||||
}
|
||||
|
||||
|
@ -181,6 +203,21 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (kDebugMode) const Spacer(),
|
||||
if (kDebugMode)
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
"Debug Height:",
|
||||
),
|
||||
const SizedBox(
|
||||
width: 2,
|
||||
),
|
||||
Text(
|
||||
ref.watch(pWalletChainHeight(widget.walletId)).toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
|
|
|
@ -52,6 +52,7 @@ import 'package:stackwallet/wallets/isar/providers/wallet_info_provider.dart';
|
|||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/firo_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
|
||||
import 'package:stackwallet/widgets/animated_text.dart';
|
||||
|
@ -1566,7 +1567,8 @@ class _DesktopSendState extends ConsumerState<DesktopSend> {
|
|||
if (!([Coin.nano, Coin.banano, Coin.epicCash, Coin.tezos]
|
||||
.contains(coin)))
|
||||
ConditionalParent(
|
||||
condition: coin.isElectrumXCoin &&
|
||||
condition: ref.watch(pWallets).getWallet(walletId)
|
||||
is ElectrumXInterface &&
|
||||
!(((coin == Coin.firo || coin == Coin.firoTestNet) &&
|
||||
(ref.watch(publicPrivateBalanceStateProvider.state).state ==
|
||||
FiroType.lelantus ||
|
||||
|
|
|
@ -10,13 +10,17 @@
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart';
|
||||
import 'package:stackwallet/providers/global/wallets_provider.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/widgets/custom_tab_view.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
class MyWallet extends ConsumerStatefulWidget {
|
||||
|
@ -40,11 +44,15 @@ class _MyWalletState extends ConsumerState<MyWallet> {
|
|||
];
|
||||
|
||||
late final bool isEth;
|
||||
late final Coin coin;
|
||||
late final bool isFrost;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
isEth = ref.read(pWallets).getWallet(widget.walletId).info.coin ==
|
||||
Coin.ethereum;
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
coin = wallet.info.coin;
|
||||
isFrost = wallet is BitcoinFrostWallet;
|
||||
isEth = coin == Coin.ethereum;
|
||||
|
||||
if (isEth && widget.contractAddress == null) {
|
||||
titles.add("Transactions");
|
||||
|
@ -64,12 +72,41 @@ class _MyWalletState extends ConsumerState<MyWallet> {
|
|||
titles: titles,
|
||||
children: [
|
||||
widget.contractAddress == null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: DesktopSend(
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
)
|
||||
? isFrost
|
||||
? Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(0, 20, 0, 0),
|
||||
child: SecondaryButton(
|
||||
width: 200,
|
||||
buttonHeight: ButtonHeight.l,
|
||||
label: "Import sign config",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
FrostImportSignConfigView.routeName,
|
||||
arguments: widget.walletId,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
FrostSendView(
|
||||
walletId: widget.walletId,
|
||||
coin: coin,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: DesktopSend(
|
||||
walletId: widget.walletId,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: DesktopTokenSend(
|
||||
|
|
|
@ -21,6 +21,7 @@ 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/wallets/wallet/impl/bitcoin_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
|
@ -80,19 +81,33 @@ class _UnlockWalletKeysDesktopState
|
|||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
final wallet = ref.read(pWallets).getWallet(widget.walletId);
|
||||
({String keys, String config})? frostData;
|
||||
List<String>? words;
|
||||
|
||||
// TODO: [prio=med] handle wallets that don't have a mnemonic
|
||||
// TODO: [prio=low] handle wallets that don't have a mnemonic
|
||||
// All wallets currently are mnemonic based
|
||||
if (wallet is! MnemonicInterface) {
|
||||
throw Exception("FIXME ~= see todo in code");
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
frostData = (
|
||||
keys: (await wallet.getSerializedKeys())!,
|
||||
config: (await wallet.getMultisigConfig())!,
|
||||
);
|
||||
print(1111111);
|
||||
print(frostData);
|
||||
} else {
|
||||
throw Exception("FIXME ~= see todo in code");
|
||||
}
|
||||
} else {
|
||||
words = await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
final words = await wallet.getMnemonicAsWords();
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.of(context).pushReplacementNamed(
|
||||
WalletKeysDesktopPopup.routeName,
|
||||
arguments: words,
|
||||
arguments: (
|
||||
mnemonic: words ?? [],
|
||||
frostData: frostData,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -301,21 +316,35 @@ class _UnlockWalletKeysDesktopState
|
|||
if (verified) {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
|
||||
({String keys, String config})? frostData;
|
||||
List<String>? words;
|
||||
|
||||
final wallet =
|
||||
ref.read(pWallets).getWallet(widget.walletId);
|
||||
|
||||
// TODO: [prio=low] handle wallets that don't have a mnemonic
|
||||
// All wallets currently are mnemonic based
|
||||
if (wallet is! MnemonicInterface) {
|
||||
throw Exception("FIXME ~= see todo in code");
|
||||
if (wallet is BitcoinFrostWallet) {
|
||||
frostData = (
|
||||
keys: (await wallet.getSerializedKeys())!,
|
||||
config: (await wallet.getMultisigConfig())!,
|
||||
);
|
||||
} else {
|
||||
throw Exception("FIXME ~= see todo in code");
|
||||
}
|
||||
} else {
|
||||
words = await wallet.getMnemonicAsWords();
|
||||
}
|
||||
|
||||
final words = await wallet.getMnemonicAsWords();
|
||||
if (mounted) {
|
||||
await Navigator.of(context)
|
||||
.pushReplacementNamed(
|
||||
WalletKeysDesktopPopup.routeName,
|
||||
arguments: words,
|
||||
arguments: (
|
||||
mnemonic: words ?? [],
|
||||
frostData: frostData,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
|
||||
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/qr_code_desktop_popup_content.dart';
|
||||
import 'package:stackwallet/themes/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/address_utils.dart';
|
||||
|
@ -24,15 +25,18 @@ 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/rounded_white_container.dart';
|
||||
|
||||
class WalletKeysDesktopPopup extends StatelessWidget {
|
||||
const WalletKeysDesktopPopup({
|
||||
Key? key,
|
||||
required this.words,
|
||||
this.frostData,
|
||||
this.clipboardInterface = const ClipboardWrapper(),
|
||||
}) : super(key: key);
|
||||
|
||||
final List<String> words;
|
||||
final ({String keys, String config})? frostData;
|
||||
final ClipboardInterface clipboardInterface;
|
||||
|
||||
static const String routeName = "walletKeysDesktopPopup";
|
||||
|
@ -66,85 +70,185 @@ class WalletKeysDesktopPopup extends StatelessWidget {
|
|||
const SizedBox(
|
||||
height: 28,
|
||||
),
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Text(
|
||||
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: MnemonicTable(
|
||||
words: words,
|
||||
isDesktop: true,
|
||||
itemBorderColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonBackSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Show QR code",
|
||||
onPressed: () {
|
||||
final String value = AddressUtils.encodeQRSeedData(words);
|
||||
Navigator.of(context).pushNamed(
|
||||
QRCodeDesktopPopupContent.routeName,
|
||||
arguments: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface.setData(
|
||||
ClipboardData(text: words.join(" ")),
|
||||
);
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
frostData != null
|
||||
? Column(
|
||||
children: [
|
||||
Text(
|
||||
"Keys",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
borderColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 9),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
frostData!.keys,
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
IconCopyButton(
|
||||
data: frostData!.keys,
|
||||
)
|
||||
// TODO [prio=low: Add QR code button and dialog.
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
"Config",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
borderColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 9),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
frostData!.config,
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
IconCopyButton(
|
||||
data: frostData!.config,
|
||||
)
|
||||
// TODO [prio=low: Add QR code button and dialog.
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Text(
|
||||
"Recovery phrase",
|
||||
style: STextStyles.desktopTextMedium(context),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Text(
|
||||
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: MnemonicTable(
|
||||
words: words,
|
||||
isDesktop: true,
|
||||
itemBorderColor: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.buttonBackSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Show QR code",
|
||||
onPressed: () {
|
||||
// TODO: address utils
|
||||
final String value =
|
||||
AddressUtils.encodeQRSeedData(words);
|
||||
Navigator.of(context).pushNamed(
|
||||
QRCodeDesktopPopupContent.routeName,
|
||||
arguments: value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Copy",
|
||||
onPressed: () async {
|
||||
await clipboardInterface.setData(
|
||||
ClipboardData(text: words.join(" ")),
|
||||
);
|
||||
if (context.mounted) {
|
||||
unawaited(
|
||||
showFloatingFlushBar(
|
||||
type: FlushBarType.info,
|
||||
message: "Copied to clipboard",
|
||||
iconAsset: Assets.svg.copy,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
|
||||
import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart';
|
||||
|
@ -34,7 +35,8 @@ enum _WalletOptions {
|
|||
changeRepresentative,
|
||||
showXpub,
|
||||
lelantusCoins,
|
||||
sparkCoins;
|
||||
sparkCoins,
|
||||
frostOptions;
|
||||
|
||||
String get prettyName {
|
||||
switch (this) {
|
||||
|
@ -50,6 +52,8 @@ enum _WalletOptions {
|
|||
return "Lelantus Coins";
|
||||
case _WalletOptions.sparkCoins:
|
||||
return "Spark Coins";
|
||||
case _WalletOptions.frostOptions:
|
||||
return "FROST settings";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +100,9 @@ class WalletOptionsButton extends StatelessWidget {
|
|||
onFiroShowSparkCoins: () async {
|
||||
Navigator.of(context).pop(_WalletOptions.sparkCoins);
|
||||
},
|
||||
onFrostMSWalletOptionsPressed: () async {
|
||||
Navigator.of(context).pop(_WalletOptions.frostOptions);
|
||||
},
|
||||
walletId: walletId,
|
||||
);
|
||||
},
|
||||
|
@ -207,6 +214,15 @@ class WalletOptionsButton extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
break;
|
||||
|
||||
case _WalletOptions.frostOptions:
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
FrostMSWalletOptionsView.routeName,
|
||||
arguments: walletId,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -241,6 +257,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
required this.onChangeRepPressed,
|
||||
required this.onFiroShowLelantusCoins,
|
||||
required this.onFiroShowSparkCoins,
|
||||
required this.onFrostMSWalletOptionsPressed,
|
||||
required this.walletId,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -250,6 +267,7 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
final VoidCallback onChangeRepPressed;
|
||||
final VoidCallback onFiroShowLelantusCoins;
|
||||
final VoidCallback onFiroShowSparkCoins;
|
||||
final VoidCallback onFrostMSWalletOptionsPressed;
|
||||
final String walletId;
|
||||
|
||||
@override
|
||||
|
@ -265,6 +283,9 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
|
||||
final bool canChangeRep = coin == Coin.nano || coin == Coin.banano;
|
||||
|
||||
final bool isFrost =
|
||||
coin == Coin.bitcoinFrost || coin == Coin.bitcoinFrostTestNet;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
|
@ -429,6 +450,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (isFrost)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
if (isFrost)
|
||||
TransparentButton(
|
||||
onPressed: onFrostMSWalletOptionsPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
Assets.svg.addressBookDesktop,
|
||||
width: 20,
|
||||
height: 20,
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textFieldActiveSearchIconLeft,
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_WalletOptions.frostOptions.prettyName,
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (xpubEnabled)
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
|
|
|
@ -105,7 +105,12 @@ class _DesktopSettingsViewState extends ConsumerState<DesktopSettingsView> {
|
|||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(15.0),
|
||||
child: SettingsMenu(),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SingleChildScrollView(
|
||||
child: SettingsMenu(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: contentViews[
|
||||
|
|
|
@ -36,305 +36,308 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint("BUILD: $runtimeType");
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 30,
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
radiusMultiplier: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.circleSliders,
|
||||
width: 48,
|
||||
height: 48,
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 30,
|
||||
),
|
||||
child: RoundedWhiteContainer(
|
||||
radiusMultiplier: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.svg.circleSliders,
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.start,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Advanced",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"\n\nConfigure these settings only if you know what you are doing!",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.start,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "Advanced",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
TextSpan(
|
||||
text:
|
||||
"\n\nConfigure these settings only if you know what you are doing!",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Toggle testnet coins",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.showTestNetCoins),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.showTestNetCoins = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Enable coin control",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.enableCoinControl),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.enableCoinControl = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
/// TODO: Make a dialog popup
|
||||
Consumer(builder: (_, ref, __) {
|
||||
final externalCalls = ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.externalCalls),
|
||||
);
|
||||
return Padding(
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Experience",
|
||||
style:
|
||||
STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
externalCalls ? "Easy crypto" : "Incognito",
|
||||
style: STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
"Toggle testnet coins",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.showTestNetCoins),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.showTestNetCoins = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Change",
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const StackPrivacyDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Block explorers",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DesktopManageBlockExplorersDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Enable coin control",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: 40,
|
||||
child: DraggableSwitchButton(
|
||||
isOn: ref.watch(
|
||||
prefsChangeNotifierProvider.select(
|
||||
(value) => value.enableCoinControl),
|
||||
),
|
||||
onValueChanged: (newValue) {
|
||||
ref
|
||||
.read(prefsChangeNotifierProvider)
|
||||
.enableCoinControl = newValue;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
|
||||
/// TODO: Make a dialog popup
|
||||
Consumer(builder: (_, ref, __) {
|
||||
final externalCalls = ref.watch(
|
||||
prefsChangeNotifierProvider
|
||||
.select((value) => value.externalCalls),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stack Experience",
|
||||
style: STextStyles.desktopTextExtraSmall(
|
||||
context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
externalCalls ? "Easy crypto" : "Incognito",
|
||||
style:
|
||||
STextStyles.desktopTextExtraExtraSmall(
|
||||
context),
|
||||
),
|
||||
],
|
||||
),
|
||||
PrimaryButton(
|
||||
label: "Change",
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const StackPrivacyDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Units",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const ManageCoinUnitsView();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Block explorers",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DesktopManageBlockExplorersDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Debug info",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Show logs",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DebugInfoDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Units",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Edit",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const ManageCoinUnitsView();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
],
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10.0),
|
||||
child: Divider(
|
||||
thickness: 0.5,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Debug info",
|
||||
style: STextStyles.desktopTextExtraSmall(context)
|
||||
.copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
.textDark),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
PrimaryButton(
|
||||
buttonHeight: ButtonHeight.xs,
|
||||
label: "Show logs",
|
||||
width: 101,
|
||||
onPressed: () async {
|
||||
await showDialog<dynamic>(
|
||||
context: context,
|
||||
useSafeArea: false,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
return const DebugInfoDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,10 @@ class _DebugInfoDialog extends ConsumerState<DebugInfoDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DesktopDialog(
|
||||
maxHeight: 850,
|
||||
// Max height of 850 unless the screen is smaller than that.
|
||||
maxHeight: MediaQuery.of(context).size.height < 850
|
||||
? MediaQuery.of(context).size.height
|
||||
: 850,
|
||||
maxWidth: 600,
|
||||
child: Column(
|
||||
children: [
|
||||
|
|
103
lib/providers/frost_wallet/frost_wallet_providers.dart
Normal file
103
lib/providers/frost_wallet/frost_wallet_providers.dart
Normal file
|
@ -0,0 +1,103 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:frostdart/frostdart_bindings_generated.dart';
|
||||
import 'package:stackwallet/services/frost.dart';
|
||||
import 'package:stackwallet/wallets/models/incomplete_frost_wallet.dart';
|
||||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||
|
||||
// =================== wallet creation =========================================
|
||||
final pFrostMultisigConfig = StateProvider<String?>((ref) => null);
|
||||
final pFrostMyName = StateProvider<String?>((ref) => null);
|
||||
|
||||
final pFrostStartKeyGenData = StateProvider<
|
||||
({
|
||||
String seed,
|
||||
String commitments,
|
||||
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
|
||||
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
|
||||
})?>((_) => null);
|
||||
|
||||
final pFrostSecretSharesData = StateProvider<
|
||||
({
|
||||
String share,
|
||||
Pointer<SecretSharesRes> secretSharesResPtr,
|
||||
})?>((ref) => null);
|
||||
|
||||
final pFrostCompletedKeyGenData = StateProvider<
|
||||
({
|
||||
Uint8List multisigId,
|
||||
String recoveryString,
|
||||
String serializedKeys,
|
||||
})?>((ref) => null);
|
||||
|
||||
// ================= transaction creation ======================================
|
||||
final pFrostTxData = StateProvider<TxData?>((ref) => null);
|
||||
|
||||
final pFrostAttemptSignData = StateProvider<
|
||||
({
|
||||
Pointer<TransactionSignMachineWrapper> machinePtr,
|
||||
String preprocess,
|
||||
})?>((ref) => null);
|
||||
|
||||
final pFrostContinueSignData = StateProvider<
|
||||
({
|
||||
Pointer<TransactionSignatureMachineWrapper> machinePtr,
|
||||
String share,
|
||||
})?>((ref) => null);
|
||||
|
||||
// ===================== shared/util ===========================================
|
||||
final pFrostSelectParticipantsUnordered =
|
||||
StateProvider<List<String>?>((ref) => null);
|
||||
|
||||
// ========================= resharing =========================================
|
||||
final pFrostResharingData = Provider((ref) => _ResharingData());
|
||||
|
||||
class _ResharingData {
|
||||
String? myName;
|
||||
|
||||
IncompleteFrostWallet? incompleteWallet;
|
||||
|
||||
// resharer encoded config string
|
||||
String? resharerConfig;
|
||||
({
|
||||
int newThreshold,
|
||||
List<int> resharers,
|
||||
List<String> newParticipants,
|
||||
})? get configData => resharerConfig != null
|
||||
? Frost.extractResharerConfigData(resharerConfig: resharerConfig!)
|
||||
: null;
|
||||
|
||||
// resharer start string (for sharing) and machine
|
||||
({
|
||||
String resharerStart,
|
||||
Pointer<StartResharerRes> machine,
|
||||
})? startResharerData;
|
||||
|
||||
// reshared start string (for sharing) and machine
|
||||
({
|
||||
String resharedStart,
|
||||
Pointer<StartResharedRes> prior,
|
||||
})? startResharedData;
|
||||
|
||||
// resharer complete string (for sharing)
|
||||
String? resharerComplete;
|
||||
|
||||
// new keys and config with an ID
|
||||
({
|
||||
String multisigConfig,
|
||||
String serializedKeys,
|
||||
String resharedId,
|
||||
})? newWalletData;
|
||||
|
||||
// reset/clear all data
|
||||
void reset() {
|
||||
resharerConfig = null;
|
||||
startResharerData = null;
|
||||
startResharedData = null;
|
||||
resharerComplete = null;
|
||||
newWalletData = null;
|
||||
incompleteWallet = null;
|
||||
}
|
||||
}
|
|
@ -26,6 +26,13 @@ import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_tok
|
|||
import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/confirm_new_frost_ms_wallet_creation_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/create_new_frost_ms_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_commitments_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/frost_share_shares_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/import_new_frost_ms_wallet_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/new/share_new_multisig_config_view.dart';
|
||||
import 'package:stackwallet/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_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';
|
||||
|
@ -76,6 +83,10 @@ import 'package:stackwallet/pages/receive_view/addresses/wallet_addresses_view.d
|
|||
import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
|
||||
import 'package:stackwallet/pages/receive_view/receive_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_attempt_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_create_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_import_sign_config_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/frost_ms/frost_send_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/send_view.dart';
|
||||
import 'package:stackwallet/pages/send_view/token_send_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart';
|
||||
|
@ -113,6 +124,19 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_pr
|
|||
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/frost_ms/frost_ms_options_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/frost_participants_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/finish_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/begin_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/complete_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1a/display_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_1b/import_reshare_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/begin_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/involved/step_2/continue_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_continue_sharing_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_import_resharer_config_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/new/new_start_resharing_view.dart';
|
||||
import 'package:stackwallet/pages/settings_views/wallet_settings_view/frost_ms/resharing/verify_updated_wallet_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';
|
||||
|
@ -423,6 +447,379 @@ class RouteGenerator {
|
|||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case CreateNewFrostMsWalletView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => CreateNewFrostMsWalletView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case RestoreFrostMsWalletView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => RestoreFrostMsWalletView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ShareNewMultisigConfigView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ShareNewMultisigConfigView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ImportNewFrostMsWalletView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ImportNewFrostMsWalletView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case NewImportResharerConfigView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => NewImportResharerConfigView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case NewStartResharingView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => NewStartResharingView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case NewContinueSharingView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => NewContinueSharingView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostShareCommitmentsView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostShareCommitmentsView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostShareSharesView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostShareSharesView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ConfirmNewFrostMSWalletCreationView.routeName:
|
||||
if (args is ({
|
||||
String walletName,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ConfirmNewFrostMSWalletCreationView(
|
||||
walletName: args.walletName,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostMSWalletOptionsView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostMSWalletOptionsView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostParticipantsView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostParticipantsView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ImportReshareConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ImportReshareConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case BeginReshareConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => BeginReshareConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case CompleteReshareConfigView.routeName:
|
||||
if (args is ({String walletId, List<int> resharers})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => CompleteReshareConfigView(
|
||||
walletId: args.walletId,
|
||||
resharers: args.resharers,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case DisplayReshareConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => DisplayReshareConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case BeginResharingView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => BeginResharingView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case ContinueResharingView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => ContinueResharingView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FinishResharingView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FinishResharingView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case VerifyUpdatedWalletView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => VerifyUpdatedWalletView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostSendView.routeName:
|
||||
if (args is ({
|
||||
String walletId,
|
||||
Coin coin,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostSendView(
|
||||
walletId: args.walletId,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostImportSignConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostImportSignConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostCreateSignConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostCreateSignConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case FrostAttemptSignConfigView.routeName:
|
||||
if (args is String) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => FrostAttemptSignConfigView(
|
||||
walletId: args,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
// case MonkeyLoadedView.routeName:
|
||||
// if (args is Tuple2<String, ChangeNotifierProvider<Manager>>) {
|
||||
// return getRoute(
|
||||
|
@ -1051,12 +1448,33 @@ class RouteGenerator {
|
|||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case WalletBackupView.routeName:
|
||||
if (args is Tuple2<String, List<String>>) {
|
||||
if (args is ({String walletId, List<String> mnemonic})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => WalletBackupView(
|
||||
walletId: args.item1,
|
||||
mnemonic: args.item2,
|
||||
walletId: args.walletId,
|
||||
mnemonic: args.mnemonic,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is ({
|
||||
String walletId,
|
||||
List<String> mnemonic,
|
||||
({
|
||||
String myName,
|
||||
String config,
|
||||
String keys,
|
||||
({String config, String keys})? prevGen,
|
||||
})? frostWalletData,
|
||||
})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => WalletBackupView(
|
||||
walletId: args.walletId,
|
||||
mnemonic: args.mnemonic,
|
||||
frostWalletData: args.frostWalletData,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
|
@ -1443,7 +1861,19 @@ class RouteGenerator {
|
|||
name: settings.name,
|
||||
),
|
||||
);
|
||||
} else if (args is ({Coin coin, String walletId})) {
|
||||
return getRoute(
|
||||
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||
builder: (_) => SendView(
|
||||
walletId: args.walletId,
|
||||
coin: args.coin,
|
||||
),
|
||||
settings: RouteSettings(
|
||||
name: settings.name,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case TokenSendView.routeName:
|
||||
|
@ -1961,10 +2391,14 @@ class RouteGenerator {
|
|||
settings: RouteSettings(name: settings.name));
|
||||
|
||||
case WalletKeysDesktopPopup.routeName:
|
||||
if (args is List<String>) {
|
||||
if (args is ({
|
||||
List<String> mnemonic,
|
||||
({String keys, String config})? frostData
|
||||
})) {
|
||||
return FadePageRoute(
|
||||
WalletKeysDesktopPopup(
|
||||
words: args,
|
||||
words: args.mnemonic,
|
||||
frostData: args.frostData,
|
||||
),
|
||||
RouteSettings(
|
||||
name: settings.name,
|
||||
|
|
|
@ -612,7 +612,7 @@ abstract class EthereumAPI {
|
|||
final response = await client.get(
|
||||
url: Uri.parse(
|
||||
// "$stackBaseServer/tokens?addrs=$contractAddress&parts=all",
|
||||
"$stackBaseServer/names?terms=$contractAddress",
|
||||
"$stackBaseServer/names?terms=$contractAddress&all",
|
||||
),
|
||||
proxyInfo: Prefs.instance.useTor
|
||||
? TorService.sharedInstance.getProxyInfo()
|
||||
|
|
613
lib/services/frost.dart
Normal file
613
lib/services/frost.dart
Normal file
|
@ -0,0 +1,613 @@
|
|||
import 'dart:ffi';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:frostdart/frostdart.dart';
|
||||
import 'package:frostdart/frostdart_bindings_generated.dart';
|
||||
import 'package:frostdart/output.dart';
|
||||
import 'package:frostdart/util.dart';
|
||||
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
abstract class Frost {
|
||||
//==================== utility ===============================================
|
||||
static List<String> getParticipants({
|
||||
required String multisigConfig,
|
||||
}) {
|
||||
try {
|
||||
final numberOfParticipants = multisigParticipants(
|
||||
multisigConfig: multisigConfig,
|
||||
);
|
||||
|
||||
final List<String> participants = [];
|
||||
for (int i = 0; i < numberOfParticipants; i++) {
|
||||
participants.add(
|
||||
multisigParticipant(
|
||||
multisigConfig: multisigConfig,
|
||||
index: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return participants;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"getParticipants failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static bool validateEncodedMultisigConfig({required String encodedConfig}) {
|
||||
try {
|
||||
decodeMultisigConfig(multisigConfig: encodedConfig);
|
||||
return true;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"validateEncodedMultisigConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int getThreshold({
|
||||
required String multisigConfig,
|
||||
}) {
|
||||
try {
|
||||
final threshold = multisigThreshold(
|
||||
multisigConfig: multisigConfig,
|
||||
);
|
||||
|
||||
return threshold;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"getThreshold failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
List<({String address, Amount amount})> recipients,
|
||||
String changeAddress,
|
||||
int feePerWeight,
|
||||
List<Output> inputs,
|
||||
}) extractDataFromSignConfig({
|
||||
required String signConfig,
|
||||
required CryptoCurrency coin,
|
||||
}) {
|
||||
try {
|
||||
final network = coin.network == CryptoCurrencyNetwork.test
|
||||
? Network.Testnet
|
||||
: Network.Mainnet;
|
||||
final signConfigPointer = decodedSignConfig(
|
||||
encodedConfig: signConfig,
|
||||
network: network,
|
||||
);
|
||||
|
||||
// get various data from config
|
||||
final feePerWeight =
|
||||
signFeePerWeight(signConfigPointer: signConfigPointer);
|
||||
final changeAddress = signChange(signConfigPointer: signConfigPointer);
|
||||
final recipientsCount = signPayments(
|
||||
signConfigPointer: signConfigPointer,
|
||||
);
|
||||
|
||||
// get tx recipient info
|
||||
final List<({String address, Amount amount})> recipients = [];
|
||||
for (int i = 0; i < recipientsCount; i++) {
|
||||
final String address = signPaymentAddress(
|
||||
signConfigPointer: signConfigPointer,
|
||||
index: i,
|
||||
);
|
||||
final int amount = signPaymentAmount(
|
||||
signConfigPointer: signConfigPointer,
|
||||
index: i,
|
||||
);
|
||||
recipients.add(
|
||||
(
|
||||
address: address,
|
||||
amount: Amount(
|
||||
rawValue: BigInt.from(amount),
|
||||
fractionDigits: coin.fractionDigits,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// get utxos
|
||||
final count = signInputs(signConfigPointer: signConfigPointer);
|
||||
final List<Output> outputs = [];
|
||||
for (int i = 0; i < count; i++) {
|
||||
final output = signInput(
|
||||
signConfig: signConfig,
|
||||
index: i,
|
||||
network: network,
|
||||
);
|
||||
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
return (
|
||||
recipients: recipients,
|
||||
changeAddress: changeAddress,
|
||||
feePerWeight: feePerWeight,
|
||||
inputs: outputs,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"extractDataFromSignConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
//==================== wallet creation =======================================
|
||||
|
||||
static String createMultisigConfig({
|
||||
required String name,
|
||||
required int threshold,
|
||||
required List<String> participants,
|
||||
}) {
|
||||
try {
|
||||
final config = newMultisigConfig(
|
||||
name: name,
|
||||
threshold: threshold,
|
||||
participants: participants,
|
||||
);
|
||||
|
||||
return config;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"createMultisigConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
String seed,
|
||||
String commitments,
|
||||
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
|
||||
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
|
||||
}) startKeyGeneration({
|
||||
required String multisigConfig,
|
||||
required String myName,
|
||||
}) {
|
||||
try {
|
||||
final startKeyGenResPtr = startKeyGen(
|
||||
multisigConfig: multisigConfig,
|
||||
myName: myName,
|
||||
language: Language.english,
|
||||
);
|
||||
|
||||
final seed = startKeyGenResPtr.ref.seed.toDartString();
|
||||
final commitments = startKeyGenResPtr.ref.commitments.toDartString();
|
||||
final configWithNamePtr = startKeyGenResPtr.ref.config;
|
||||
final machinePtr = startKeyGenResPtr.ref.machine;
|
||||
|
||||
return (
|
||||
seed: seed,
|
||||
commitments: commitments,
|
||||
multisigConfigWithNamePtr: configWithNamePtr,
|
||||
secretShareMachineWrapperPtr: machinePtr,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"startKeyGeneration failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
String share,
|
||||
Pointer<SecretSharesRes> secretSharesResPtr,
|
||||
}) generateSecretShares({
|
||||
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
|
||||
required String mySeed,
|
||||
required Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
|
||||
required List<String> commitments,
|
||||
}) {
|
||||
try {
|
||||
final secretSharesResPtr = getSecretShares(
|
||||
multisigConfigWithName: multisigConfigWithNamePtr,
|
||||
seed: mySeed,
|
||||
language: Language.english,
|
||||
machine: secretShareMachineWrapperPtr,
|
||||
commitments: commitments,
|
||||
);
|
||||
|
||||
final share = secretSharesResPtr.ref.shares.toDartString();
|
||||
|
||||
return (share: share, secretSharesResPtr: secretSharesResPtr);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"generateSecretShares failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
Uint8List multisigId,
|
||||
String recoveryString,
|
||||
String serializedKeys,
|
||||
}) completeKeyGeneration({
|
||||
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
|
||||
required Pointer<SecretSharesRes> secretSharesResPtr,
|
||||
required List<String> shares,
|
||||
}) {
|
||||
try {
|
||||
final keyGenResPtr = completeKeyGen(
|
||||
multisigConfigWithName: multisigConfigWithNamePtr,
|
||||
machineAndCommitments: secretSharesResPtr,
|
||||
shares: shares,
|
||||
);
|
||||
|
||||
final id = Uint8List.fromList(
|
||||
List<int>.generate(
|
||||
MULTISIG_ID_LENGTH,
|
||||
(index) => keyGenResPtr.ref.multisig_id[index],
|
||||
),
|
||||
);
|
||||
|
||||
final recoveryString = keyGenResPtr.ref.recovery.toDartString();
|
||||
|
||||
final serializedKeys = serializeKeys(keys: keyGenResPtr.ref.keys);
|
||||
|
||||
return (
|
||||
multisigId: id,
|
||||
recoveryString: recoveryString,
|
||||
serializedKeys: serializedKeys,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"completeKeyGeneration failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
//=================== transaction creation ===================================
|
||||
|
||||
static String createSignConfig({
|
||||
required int network,
|
||||
required List<({UTXO utxo, Uint8List scriptPubKey})> inputs,
|
||||
required List<({String address, Amount amount, bool isChange})> outputs,
|
||||
required String changeAddress,
|
||||
required int feePerWeight,
|
||||
}) {
|
||||
try {
|
||||
final signConfig = newSignConfig(
|
||||
network: network,
|
||||
outputs: inputs
|
||||
.map(
|
||||
(e) => Output(
|
||||
hash: e.utxo.txid.toUint8ListFromHex,
|
||||
vout: e.utxo.vout,
|
||||
value: e.utxo.value,
|
||||
scriptPubKey: e.scriptPubKey,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
paymentAddresses: outputs.map((e) => e.address).toList(),
|
||||
paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(),
|
||||
change: changeAddress,
|
||||
feePerWeight: feePerWeight,
|
||||
);
|
||||
|
||||
return signConfig;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"createSignConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
Pointer<TransactionSignMachineWrapper> machinePtr,
|
||||
String preprocess,
|
||||
}) attemptSignConfig({
|
||||
required int network,
|
||||
required String config,
|
||||
required String serializedKeys,
|
||||
}) {
|
||||
try {
|
||||
final keys = deserializeKeys(keys: serializedKeys);
|
||||
|
||||
final attemptSignRes = attemptSign(
|
||||
thresholdKeysWrapperPointer: keys,
|
||||
network: network,
|
||||
signConfig: config,
|
||||
);
|
||||
|
||||
return (
|
||||
preprocess: attemptSignRes.ref.preprocess.toDartString(),
|
||||
machinePtr: attemptSignRes.ref.machine,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"attemptSignConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
Pointer<TransactionSignatureMachineWrapper> machinePtr,
|
||||
String share,
|
||||
}) continueSigning({
|
||||
required Pointer<TransactionSignMachineWrapper> machinePtr,
|
||||
required List<String> preprocesses,
|
||||
}) {
|
||||
try {
|
||||
final continueSignRes = continueSign(
|
||||
machine: machinePtr,
|
||||
preprocesses: preprocesses,
|
||||
);
|
||||
|
||||
return (
|
||||
share: continueSignRes.ref.preprocess.toDartString(),
|
||||
machinePtr: continueSignRes.ref.machine,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"continueSigning failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static String completeSigning({
|
||||
required Pointer<TransactionSignatureMachineWrapper> machinePtr,
|
||||
required List<String> shares,
|
||||
}) {
|
||||
try {
|
||||
final rawTransaction = completeSign(
|
||||
machine: machinePtr,
|
||||
shares: shares,
|
||||
);
|
||||
|
||||
return rawTransaction;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"completeSigning failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Pointer<SignConfig> decodedSignConfig({
|
||||
required String encodedConfig,
|
||||
required int network,
|
||||
}) {
|
||||
try {
|
||||
final configPtr =
|
||||
decodeSignConfig(encodedSignConfig: encodedConfig, network: network);
|
||||
return configPtr;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"decodedSignConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
//========================== resharing =======================================
|
||||
|
||||
static String createResharerConfig({
|
||||
required int newThreshold,
|
||||
required List<int> resharers,
|
||||
required List<String> newParticipants,
|
||||
}) {
|
||||
try {
|
||||
final config = newResharerConfig(
|
||||
newThreshold: newThreshold,
|
||||
newParticipants: newParticipants,
|
||||
resharers: resharers,
|
||||
);
|
||||
|
||||
return config;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"createResharerConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
String resharerStart,
|
||||
Pointer<StartResharerRes> machine,
|
||||
}) beginResharer({
|
||||
required String serializedKeys,
|
||||
required String config,
|
||||
}) {
|
||||
try {
|
||||
final result = startResharer(
|
||||
serializedKeys: serializedKeys,
|
||||
config: config,
|
||||
);
|
||||
|
||||
return (
|
||||
resharerStart: result.encoded,
|
||||
machine: result.machine,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"beginResharer failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// expects [resharerStarts] of length equal to resharers.
|
||||
static ({
|
||||
String resharedStart,
|
||||
Pointer<StartResharedRes> prior,
|
||||
}) beginReshared({
|
||||
required String myName,
|
||||
required String resharerConfig,
|
||||
required List<String> resharerStarts,
|
||||
}) {
|
||||
try {
|
||||
final result = startReshared(
|
||||
newMultisigName: 'unused_property',
|
||||
myName: myName,
|
||||
resharerConfig: resharerConfig,
|
||||
resharerStarts: resharerStarts,
|
||||
);
|
||||
return (
|
||||
resharedStart: result.encoded,
|
||||
prior: result.machine,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"beginReshared failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// expects [encryptionKeysOfResharedTo] of length equal to new participants
|
||||
static String finishResharer({
|
||||
required StartResharerRes machine,
|
||||
required List<String> encryptionKeysOfResharedTo,
|
||||
}) {
|
||||
try {
|
||||
final result = completeResharer(
|
||||
machine: machine,
|
||||
encryptionKeysOfResharedTo: encryptionKeysOfResharedTo,
|
||||
);
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"finishResharer failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// expects [resharerCompletes] of length equal to resharers
|
||||
static ({
|
||||
String multisigConfig,
|
||||
String serializedKeys,
|
||||
String resharedId,
|
||||
}) finishReshared({
|
||||
required StartResharedRes prior,
|
||||
required List<String> resharerCompletes,
|
||||
}) {
|
||||
try {
|
||||
final result = completeReshared(
|
||||
prior: prior,
|
||||
resharerCompletes: resharerCompletes,
|
||||
);
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"finishReshared failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Pointer<ResharerConfig> decodedResharerConfig({
|
||||
required String resharerConfig,
|
||||
}) {
|
||||
try {
|
||||
final config = decodeResharerConfig(resharerConfig: resharerConfig);
|
||||
|
||||
return config;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"decodedResharerConfig failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static ({
|
||||
int newThreshold,
|
||||
List<int> resharers,
|
||||
List<String> newParticipants,
|
||||
}) extractResharerConfigData({
|
||||
required String resharerConfig,
|
||||
}) {
|
||||
try {
|
||||
final newThreshold = resharerNewThreshold(
|
||||
resharerConfigPointer: decodedResharerConfig(
|
||||
resharerConfig: resharerConfig,
|
||||
),
|
||||
);
|
||||
|
||||
final resharersCount = resharerResharers(
|
||||
resharerConfigPointer: decodedResharerConfig(
|
||||
resharerConfig: resharerConfig,
|
||||
),
|
||||
);
|
||||
final List<int> resharers = [];
|
||||
for (int i = 0; i < resharersCount; i++) {
|
||||
resharers.add(
|
||||
resharerResharer(
|
||||
resharerConfigPointer: decodedResharerConfig(
|
||||
resharerConfig: resharerConfig,
|
||||
),
|
||||
index: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final newParticipantsCount = resharerNewParticipants(
|
||||
resharerConfigPointer: decodedResharerConfig(
|
||||
resharerConfig: resharerConfig,
|
||||
),
|
||||
);
|
||||
final List<String> newParticipants = [];
|
||||
for (int i = 0; i < newParticipantsCount; i++) {
|
||||
newParticipants.add(
|
||||
resharerNewParticipant(
|
||||
resharerConfigPointer: decodedResharerConfig(
|
||||
resharerConfig: resharerConfig,
|
||||
),
|
||||
index: i,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
newThreshold: newThreshold,
|
||||
resharers: resharers,
|
||||
newParticipants: newParticipants,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"extractResharerConfigData failed: $e\n$s",
|
||||
level: LogLevel.Fatal,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import 'package:stackwallet/services/wallets.dart';
|
|||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart';
|
||||
|
||||
import 'exchange/exchange.dart';
|
||||
|
||||
|
@ -123,7 +124,7 @@ class NotificationsService extends ChangeNotifier {
|
|||
|
||||
final node = nodeService.getPrimaryNodeFor(coin: coin);
|
||||
if (node != null) {
|
||||
if (coin.isElectrumXCoin) {
|
||||
if (wallet is ElectrumXInterface) {
|
||||
final eNode = ElectrumXNode(
|
||||
address: node.host,
|
||||
port: node.port,
|
||||
|
@ -146,6 +147,7 @@ class NotificationsService extends ChangeNotifier {
|
|||
node: eNode,
|
||||
failovers: failovers,
|
||||
prefs: prefs,
|
||||
coin: coin,
|
||||
);
|
||||
final tx = await client.getTransaction(txHash: txid);
|
||||
|
||||
|
|
|
@ -42,7 +42,13 @@ class Wallets {
|
|||
|
||||
final Map<String, Wallet> _wallets = {};
|
||||
|
||||
Wallet getWallet(String walletId) => _wallets[walletId]!;
|
||||
Wallet getWallet(String walletId) {
|
||||
if (_wallets[walletId] != null) {
|
||||
return _wallets[walletId]!;
|
||||
} else {
|
||||
throw Exception("Wallet with id $walletId not found");
|
||||
}
|
||||
}
|
||||
|
||||
void addWallet(Wallet wallet) {
|
||||
if (_wallets[wallet.walletId] != null) {
|
||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|||
final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
|
||||
final assets = ref.watch(themeAssetsProvider);
|
||||
|
||||
// TODO: handle this differently by adding proper frost assets to themes
|
||||
if (coin == Coin.bitcoinFrost) {
|
||||
coin = Coin.bitcoin;
|
||||
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||
coin = Coin.bitcoinTestNet;
|
||||
}
|
||||
|
||||
if (assets is ThemeAssetsV3) {
|
||||
return assets.coinCardImages?[coin.mainNetVersion];
|
||||
} else {
|
||||
|
@ -26,6 +33,13 @@ final coinCardProvider = Provider.family<String?, Coin>((ref, coin) {
|
|||
final coinCardFavoritesProvider = Provider.family<String?, Coin>((ref, coin) {
|
||||
final assets = ref.watch(themeAssetsProvider);
|
||||
|
||||
// TODO: handle this differently by adding proper frost assets to themes
|
||||
if (coin == Coin.bitcoinFrost) {
|
||||
coin = Coin.bitcoin;
|
||||
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||
coin = Coin.bitcoinTestNet;
|
||||
}
|
||||
|
||||
if (assets is ThemeAssetsV3) {
|
||||
return assets.coinCardFavoritesImages?[coin.mainNetVersion] ??
|
||||
assets.coinCardImages?[coin.mainNetVersion];
|
||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|||
final coinIconProvider = Provider.family<String, Coin>((ref, coin) {
|
||||
final assets = ref.watch(themeAssetsProvider);
|
||||
|
||||
// TODO: handle this differently by adding proper frost assets to themes
|
||||
if (coin == Coin.bitcoinFrost) {
|
||||
coin = Coin.bitcoin;
|
||||
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||
coin = Coin.bitcoinTestNet;
|
||||
}
|
||||
|
||||
if (assets is ThemeAssets) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
|
|
|
@ -16,6 +16,13 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|||
final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
|
||||
final assets = ref.watch(themeAssetsProvider);
|
||||
|
||||
// TODO: handle this differently by adding proper frost assets to themes
|
||||
if (coin == Coin.bitcoinFrost) {
|
||||
coin = Coin.bitcoin;
|
||||
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||
coin = Coin.bitcoinTestNet;
|
||||
}
|
||||
|
||||
if (assets is ThemeAssets) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
|
@ -64,6 +71,13 @@ final coinImageProvider = Provider.family<String, Coin>((ref, coin) {
|
|||
final coinImageSecondaryProvider = Provider.family<String, Coin>((ref, coin) {
|
||||
final assets = ref.watch(themeAssetsProvider);
|
||||
|
||||
// TODO: handle this differently by adding proper frost assets to themes
|
||||
if (coin == Coin.bitcoinFrost) {
|
||||
coin = Coin.bitcoin;
|
||||
} else if (coin == Coin.bitcoinFrostTestNet) {
|
||||
coin = Coin.bitcoinTestNet;
|
||||
}
|
||||
|
||||
if (assets is ThemeAssets) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
|
|
|
@ -37,6 +37,8 @@ class CoinThemeColorDefault {
|
|||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return bitcoin;
|
||||
case Coin.litecoin:
|
||||
case Coin.litecoinTestNet:
|
||||
|
|
|
@ -1680,6 +1680,8 @@ class StackColors extends ThemeExtension<StackColors> {
|
|||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return _coin.bitcoin;
|
||||
case Coin.litecoin:
|
||||
case Coin.litecoinTestNet:
|
||||
|
|
|
@ -14,6 +14,24 @@ import 'package:bitcoindart/bitcoindart.dart';
|
|||
import 'package:crypto/crypto.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/banano.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin_frost.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/dogecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/firo.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/litecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/monero.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/namecoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/nano.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/particl.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/stellar.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/tezos.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||
|
||||
class AddressUtils {
|
||||
static String condenseAddress(String address) {
|
||||
|
@ -49,7 +67,61 @@ class AddressUtils {
|
|||
}
|
||||
|
||||
static bool validateAddress(String address, Coin coin) {
|
||||
throw Exception("moved");
|
||||
//This calls the validate address for each crypto coin, validateAddress is
|
||||
//only used in 2 places, so I just replaced the old functionality here
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
return Bitcoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.bitcoinFrost:
|
||||
return BitcoinFrost(CryptoCurrencyNetwork.main)
|
||||
.validateAddress(address);
|
||||
case Coin.litecoin:
|
||||
return Litecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.bitcoincash:
|
||||
return Bitcoincash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.dogecoin:
|
||||
return Dogecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.epicCash:
|
||||
return Epiccash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.ethereum:
|
||||
return Ethereum(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.firo:
|
||||
return Firo(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.eCash:
|
||||
return Ecash(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.monero:
|
||||
return Monero(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.wownero:
|
||||
return Wownero(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.namecoin:
|
||||
return Namecoin(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.particl:
|
||||
return Particl(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.stellar:
|
||||
return Stellar(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.nano:
|
||||
return Nano(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.banano:
|
||||
return Banano(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.tezos:
|
||||
return Tezos(CryptoCurrencyNetwork.main).validateAddress(address);
|
||||
case Coin.bitcoinTestNet:
|
||||
return Bitcoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return BitcoinFrost(CryptoCurrencyNetwork.test)
|
||||
.validateAddress(address);
|
||||
case Coin.litecoinTestNet:
|
||||
return Litecoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.bitcoincashTestnet:
|
||||
return Bitcoincash(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.firoTestNet:
|
||||
return Firo(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.dogecoinTestNet:
|
||||
return Dogecoin(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
case Coin.stellarTestnet:
|
||||
return Stellar(CryptoCurrencyNetwork.test).validateAddress(address);
|
||||
}
|
||||
// throw Exception("moved");
|
||||
// switch (coin) {
|
||||
// case Coin.bitcoin:
|
||||
// return Address.validateAddress(address, bitcoin);
|
||||
|
|
|
@ -40,6 +40,8 @@ enum AmountUnit {
|
|||
case Coin.litecoin:
|
||||
case Coin.particl:
|
||||
case Coin.namecoin:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
|
|
|
@ -257,6 +257,7 @@ class _PNG {
|
|||
|
||||
String get glasses => "assets/images/glasses.png";
|
||||
String get glassesHidden => "assets/images/glasses-hidden.png";
|
||||
String get mascot => "assets/images/mascot.png";
|
||||
}
|
||||
|
||||
class _ANIMATIONS {
|
||||
|
|
|
@ -18,6 +18,7 @@ Uri getDefaultBlockExplorerUrlFor({
|
|||
required String txid,
|
||||
}) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoin:
|
||||
return Uri.parse("https://mempool.space/tx/$txid");
|
||||
case Coin.litecoin:
|
||||
|
@ -25,11 +26,12 @@ Uri getDefaultBlockExplorerUrlFor({
|
|||
case Coin.litecoinTestNet:
|
||||
return Uri.parse("https://chain.so/tx/LTCTEST/$txid");
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return Uri.parse("https://mempool.space/testnet/tx/$txid");
|
||||
case Coin.dogecoin:
|
||||
return Uri.parse("https://chain.so/tx/DOGE/$txid");
|
||||
case Coin.eCash:
|
||||
return Uri.parse("https://explorer.bitcoinabc.org/tx/$txid");
|
||||
return Uri.parse("https://explorer.e.cash/tx/$txid");
|
||||
case Coin.dogecoinTestNet:
|
||||
return Uri.parse("https://chain.so/tx/DOGETEST/$txid");
|
||||
case Coin.epicCash:
|
||||
|
|
|
@ -69,6 +69,7 @@ abstract class Constants {
|
|||
static BigInt satsPerCoin(Coin coin) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.litecoin:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincash:
|
||||
|
@ -76,6 +77,7 @@ abstract class Constants {
|
|||
case Coin.dogecoin:
|
||||
case Coin.firo:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.dogecoinTestNet:
|
||||
case Coin.firoTestNet:
|
||||
case Coin.epicCash:
|
||||
|
@ -113,6 +115,7 @@ abstract class Constants {
|
|||
static int decimalPlacesForCoin(Coin coin) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.litecoin:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincash:
|
||||
|
@ -120,6 +123,7 @@ abstract class Constants {
|
|||
case Coin.dogecoin:
|
||||
case Coin.firo:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.dogecoinTestNet:
|
||||
case Coin.firoTestNet:
|
||||
case Coin.epicCash:
|
||||
|
@ -189,6 +193,10 @@ abstract class Constants {
|
|||
case Coin.wownero:
|
||||
values.addAll([14, 25]);
|
||||
break;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
throw ArgumentError("Frost mnemonic lengths unsupported");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
@ -198,6 +206,8 @@ abstract class Constants {
|
|||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.bitcoincashTestnet:
|
||||
case Coin.eCash:
|
||||
|
@ -277,6 +287,10 @@ abstract class Constants {
|
|||
|
||||
case Coin.monero:
|
||||
return 25;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
throw ArgumentError("Frost mnemonic length unsupported");
|
||||
//
|
||||
// default:
|
||||
// -1;
|
||||
|
|
|
@ -17,7 +17,7 @@ abstract class DefaultEpicBoxes {
|
|||
static List<String> get defaultIds => ['americas', 'asia', 'europe'];
|
||||
|
||||
static EpicBoxServerModel get americas => EpicBoxServerModel(
|
||||
host: 'epicbox.epic.tech',
|
||||
host: 'epicbox.stackwallet.com',
|
||||
port: 443,
|
||||
name: 'Americas',
|
||||
id: 'americas',
|
||||
|
|
|
@ -312,6 +312,7 @@ abstract class DefaultNodes {
|
|||
static NodeModel getNodeFor(Coin coin) {
|
||||
switch (coin) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
return bitcoin;
|
||||
|
||||
case Coin.litecoin:
|
||||
|
@ -360,6 +361,7 @@ abstract class DefaultNodes {
|
|||
return tezos;
|
||||
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return bitcoinTestnet;
|
||||
|
||||
case Coin.litecoinTestNet:
|
||||
|
|
|
@ -16,6 +16,7 @@ enum Coin {
|
|||
monero,
|
||||
banano,
|
||||
bitcoincash,
|
||||
bitcoinFrost,
|
||||
dogecoin,
|
||||
eCash,
|
||||
epicCash,
|
||||
|
@ -36,6 +37,7 @@ enum Coin {
|
|||
|
||||
bitcoinTestNet,
|
||||
bitcoincashTestnet,
|
||||
bitcoinFrostTestNet,
|
||||
dogecoinTestNet,
|
||||
firoTestNet,
|
||||
litecoinTestNet,
|
||||
|
@ -47,6 +49,8 @@ extension CoinExt on Coin {
|
|||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
return "Bitcoin";
|
||||
case Coin.bitcoinFrost:
|
||||
return "Bitcoin Frost";
|
||||
case Coin.litecoin:
|
||||
return "Litecoin";
|
||||
case Coin.bitcoincash:
|
||||
|
@ -79,6 +83,8 @@ extension CoinExt on Coin {
|
|||
return "Banano";
|
||||
case Coin.bitcoinTestNet:
|
||||
return "tBitcoin";
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return "tBitcoin Frost";
|
||||
case Coin.litecoinTestNet:
|
||||
return "tLitecoin";
|
||||
case Coin.bitcoincashTestnet:
|
||||
|
@ -95,6 +101,7 @@ extension CoinExt on Coin {
|
|||
String get ticker {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
return "BTC";
|
||||
case Coin.litecoin:
|
||||
return "LTC";
|
||||
|
@ -127,6 +134,7 @@ extension CoinExt on Coin {
|
|||
case Coin.banano:
|
||||
return "BAN";
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return "tBTC";
|
||||
case Coin.litecoinTestNet:
|
||||
return "tLTC";
|
||||
|
@ -144,6 +152,7 @@ extension CoinExt on Coin {
|
|||
String get uriScheme {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
return "bitcoin";
|
||||
case Coin.litecoin:
|
||||
return "litecoin";
|
||||
|
@ -177,6 +186,7 @@ extension CoinExt on Coin {
|
|||
case Coin.banano:
|
||||
return "ban";
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return "bitcoin";
|
||||
case Coin.litecoinTestNet:
|
||||
return "litecoin";
|
||||
|
@ -191,36 +201,6 @@ extension CoinExt on Coin {
|
|||
}
|
||||
}
|
||||
|
||||
bool get isElectrumXCoin {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.litecoin:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.dogecoin:
|
||||
case Coin.firo:
|
||||
case Coin.namecoin:
|
||||
case Coin.particl:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
case Coin.firoTestNet:
|
||||
case Coin.dogecoinTestNet:
|
||||
case Coin.eCash:
|
||||
return true;
|
||||
|
||||
case Coin.epicCash:
|
||||
case Coin.ethereum:
|
||||
case Coin.monero:
|
||||
case Coin.tezos:
|
||||
case Coin.wownero:
|
||||
case Coin.nano:
|
||||
case Coin.banano:
|
||||
case Coin.stellar:
|
||||
case Coin.stellarTestnet:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasMnemonicPassphraseSupport {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
|
@ -241,6 +221,8 @@ extension CoinExt on Coin {
|
|||
case Coin.stellarTestnet:
|
||||
return true;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.epicCash:
|
||||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
|
@ -260,6 +242,8 @@ extension CoinExt on Coin {
|
|||
case Coin.ethereum:
|
||||
return true;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.firo:
|
||||
case Coin.namecoin:
|
||||
case Coin.particl:
|
||||
|
@ -284,6 +268,7 @@ extension CoinExt on Coin {
|
|||
bool get isTestNet {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.litecoin:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.dogecoin:
|
||||
|
@ -303,6 +288,7 @@ extension CoinExt on Coin {
|
|||
|
||||
case Coin.dogecoinTestNet:
|
||||
case Coin.bitcoinTestNet:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.litecoinTestNet:
|
||||
case Coin.bitcoincashTestnet:
|
||||
case Coin.firoTestNet:
|
||||
|
@ -311,9 +297,21 @@ extension CoinExt on Coin {
|
|||
}
|
||||
}
|
||||
|
||||
bool get isFrost {
|
||||
switch (this) {
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Coin get mainNetVersion {
|
||||
switch (this) {
|
||||
case Coin.bitcoin:
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.litecoin:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.dogecoin:
|
||||
|
@ -337,6 +335,9 @@ extension CoinExt on Coin {
|
|||
case Coin.bitcoinTestNet:
|
||||
return Coin.bitcoin;
|
||||
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return Coin.bitcoinFrost;
|
||||
|
||||
case Coin.litecoinTestNet:
|
||||
return Coin.litecoin;
|
||||
|
||||
|
@ -364,6 +365,10 @@ extension CoinExt on Coin {
|
|||
case Coin.particl:
|
||||
return AddressType.p2wpkh;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
return AddressType.frostMS;
|
||||
|
||||
case Coin.eCash:
|
||||
case Coin.bitcoincash:
|
||||
case Coin.bitcoincashTestnet:
|
||||
|
@ -501,6 +506,15 @@ Coin coinFromPrettyName(String name) {
|
|||
case "tStellar":
|
||||
return Coin.stellarTestnet;
|
||||
|
||||
case "Bitcoin Frost":
|
||||
case "bitcoinFrost":
|
||||
return Coin.bitcoinFrost;
|
||||
|
||||
case "Bitcoin Frost Testnet":
|
||||
case "tBitcoin Frost":
|
||||
case "bitcoinFrostTestNet":
|
||||
return Coin.bitcoinFrostTestNet;
|
||||
|
||||
default:
|
||||
throw ArgumentError.value(
|
||||
name,
|
||||
|
|
|
@ -44,6 +44,8 @@ extension DerivePathTypeExt on DerivePathType {
|
|||
case Coin.ethereum: // TODO: do we need something here?
|
||||
return DerivePathType.eth;
|
||||
|
||||
case Coin.bitcoinFrost:
|
||||
case Coin.bitcoinFrostTestNet:
|
||||
case Coin.epicCash:
|
||||
case Coin.monero:
|
||||
case Coin.wownero:
|
||||
|
|
|
@ -68,6 +68,7 @@ class Prefs extends ChangeNotifier {
|
|||
await _setMaxDecimals();
|
||||
_useTor = await _getUseTor();
|
||||
_fusionServerInfo = await _getFusionServerInfo();
|
||||
_frostEnabled = await _getFrostEnabled();
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
@ -1008,4 +1009,25 @@ class Prefs extends ChangeNotifier {
|
|||
|
||||
return actualMap;
|
||||
}
|
||||
|
||||
// FROST multisig
|
||||
|
||||
bool _frostEnabled = false;
|
||||
|
||||
bool get frostEnabled => _frostEnabled;
|
||||
|
||||
set frostEnabled(bool frostEnabled) {
|
||||
if (_frostEnabled != frostEnabled) {
|
||||
DB.instance.put<dynamic>(
|
||||
boxName: DB.boxNamePrefs, key: "frostEnabled", value: frostEnabled);
|
||||
_frostEnabled = frostEnabled;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _getFrostEnabled() async {
|
||||
return await DB.instance.get<dynamic>(
|
||||
boxName: DB.boxNamePrefs, key: "frostEnabled") as bool? ??
|
||||
false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,30 +170,10 @@ class Bitcoin extends Bip39HDCurrency with PaynymCurrencyInterface {
|
|||
NodeModel get defaultNode {
|
||||
switch (network) {
|
||||
case CryptoCurrencyNetwork.main:
|
||||
return NodeModel(
|
||||
host: "bitcoin.stackwallet.com",
|
||||
port: 50002,
|
||||
name: DefaultNodes.defaultName,
|
||||
id: DefaultNodes.buildId(Coin.bitcoin),
|
||||
useSSL: true,
|
||||
enabled: true,
|
||||
coinName: Coin.bitcoin.name,
|
||||
isFailover: true,
|
||||
isDown: false,
|
||||
);
|
||||
return DefaultNodes.bitcoin;
|
||||
|
||||
case CryptoCurrencyNetwork.test:
|
||||
return NodeModel(
|
||||
host: "bitcoin-testnet.stackwallet.com",
|
||||
port: 51002,
|
||||
name: DefaultNodes.defaultName,
|
||||
id: DefaultNodes.buildId(Coin.bitcoinTestNet),
|
||||
useSSL: true,
|
||||
enabled: true,
|
||||
coinName: Coin.bitcoinTestNet.name,
|
||||
isFailover: true,
|
||||
isDown: false,
|
||||
);
|
||||
return DefaultNodes.bitcoinTestnet;
|
||||
|
||||
default:
|
||||
throw UnimplementedError();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue