diff --git a/analysis_options.yaml b/analysis_options.yaml index ea46ed3ca..db030aa14 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -92,7 +92,7 @@ linter: constant_identifier_names: false prefer_final_locals: true prefer_final_in_for_each: true - require_trailing_commas: true +# require_trailing_commas: true // causes issues with dart 3.7 # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule diff --git a/lib/db/drift/database.dart b/lib/db/drift/database.dart new file mode 100644 index 000000000..36cb67150 --- /dev/null +++ b/lib/db/drift/database.dart @@ -0,0 +1,87 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-05-06 + * + */ + +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:path/path.dart' as path; + +import '../../utilities/stack_file_system.dart'; + +part 'database.g.dart'; + +abstract final class Drift { + static bool _didInit = false; + + static final Map _map = {}; + + static WalletDatabase get(String walletId) { + if (!_didInit) { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + _didInit = true; + } + + return _map[walletId] ??= WalletDatabase._(walletId); + } +} + +class SparkNames extends Table { + TextColumn get name => + text().customConstraint("UNIQUE NOT NULL COLLATE NOCASE")(); + TextColumn get address => text()(); + IntColumn get validUntil => integer()(); + TextColumn get additionalInfo => text().nullable()(); + + @override + Set get primaryKey => {name}; +} + +@DriftDatabase(tables: [SparkNames]) +final class WalletDatabase extends _$WalletDatabase { + WalletDatabase._(String walletId, [QueryExecutor? executor]) + : super(executor ?? _openConnection(walletId)); + + @override + int get schemaVersion => 1; + + static QueryExecutor _openConnection(String walletId) { + return driftDatabase( + name: walletId, + native: DriftNativeOptions( + shareAcrossIsolates: true, + databasePath: () async { + final dir = await StackFileSystem.applicationDriftDirectory(); + return path.join(dir.path, "wallets", walletId, "$walletId.db"); + }, + ), + ); + } + + Future upsertSparkNames( + List< + ({String name, String address, int validUntil, String? additionalInfo}) + > + names, + ) async { + await transaction(() async { + for (final name in names) { + await into(sparkNames).insertOnConflictUpdate( + SparkNamesCompanion( + name: Value(name.name), + address: Value(name.address), + validUntil: Value(name.validUntil), + additionalInfo: Value(name.additionalInfo), + ), + ); + } + }); + } +} diff --git a/lib/db/drift/database.g.dart b/lib/db/drift/database.g.dart new file mode 100644 index 000000000..42113b071 --- /dev/null +++ b/lib/db/drift/database.g.dart @@ -0,0 +1,459 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $SparkNamesTable extends SparkNames + with TableInfo<$SparkNamesTable, SparkName> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $SparkNamesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'UNIQUE NOT NULL COLLATE NOCASE'); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _validUntilMeta = + const VerificationMeta('validUntil'); + @override + late final GeneratedColumn validUntil = GeneratedColumn( + 'valid_until', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _additionalInfoMeta = + const VerificationMeta('additionalInfo'); + @override + late final GeneratedColumn additionalInfo = GeneratedColumn( + 'additional_info', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => + [name, address, validUntil, additionalInfo]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'spark_names'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('address')) { + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); + } else if (isInserting) { + context.missing(_addressMeta); + } + if (data.containsKey('valid_until')) { + context.handle( + _validUntilMeta, + validUntil.isAcceptableOrUnknown( + data['valid_until']!, _validUntilMeta)); + } else if (isInserting) { + context.missing(_validUntilMeta); + } + if (data.containsKey('additional_info')) { + context.handle( + _additionalInfoMeta, + additionalInfo.isAcceptableOrUnknown( + data['additional_info']!, _additionalInfoMeta)); + } + return context; + } + + @override + Set get $primaryKey => {name}; + @override + SparkName map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return SparkName( + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + validUntil: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}valid_until'])!, + additionalInfo: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}additional_info']), + ); + } + + @override + $SparkNamesTable createAlias(String alias) { + return $SparkNamesTable(attachedDatabase, alias); + } +} + +class SparkName extends DataClass implements Insertable { + final String name; + final String address; + final int validUntil; + final String? additionalInfo; + const SparkName( + {required this.name, + required this.address, + required this.validUntil, + this.additionalInfo}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = Variable(name); + map['address'] = Variable(address); + map['valid_until'] = Variable(validUntil); + if (!nullToAbsent || additionalInfo != null) { + map['additional_info'] = Variable(additionalInfo); + } + return map; + } + + SparkNamesCompanion toCompanion(bool nullToAbsent) { + return SparkNamesCompanion( + name: Value(name), + address: Value(address), + validUntil: Value(validUntil), + additionalInfo: additionalInfo == null && nullToAbsent + ? const Value.absent() + : Value(additionalInfo), + ); + } + + factory SparkName.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return SparkName( + name: serializer.fromJson(json['name']), + address: serializer.fromJson(json['address']), + validUntil: serializer.fromJson(json['validUntil']), + additionalInfo: serializer.fromJson(json['additionalInfo']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'address': serializer.toJson(address), + 'validUntil': serializer.toJson(validUntil), + 'additionalInfo': serializer.toJson(additionalInfo), + }; + } + + SparkName copyWith( + {String? name, + String? address, + int? validUntil, + Value additionalInfo = const Value.absent()}) => + SparkName( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: + additionalInfo.present ? additionalInfo.value : this.additionalInfo, + ); + SparkName copyWithCompanion(SparkNamesCompanion data) { + return SparkName( + name: data.name.present ? data.name.value : this.name, + address: data.address.present ? data.address.value : this.address, + validUntil: + data.validUntil.present ? data.validUntil.value : this.validUntil, + additionalInfo: data.additionalInfo.present + ? data.additionalInfo.value + : this.additionalInfo, + ); + } + + @override + String toString() { + return (StringBuffer('SparkName(') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('validUntil: $validUntil, ') + ..write('additionalInfo: $additionalInfo') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, address, validUntil, additionalInfo); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SparkName && + other.name == this.name && + other.address == this.address && + other.validUntil == this.validUntil && + other.additionalInfo == this.additionalInfo); +} + +class SparkNamesCompanion extends UpdateCompanion { + final Value name; + final Value address; + final Value validUntil; + final Value additionalInfo; + final Value rowid; + const SparkNamesCompanion({ + this.name = const Value.absent(), + this.address = const Value.absent(), + this.validUntil = const Value.absent(), + this.additionalInfo = const Value.absent(), + this.rowid = const Value.absent(), + }); + SparkNamesCompanion.insert({ + required String name, + required String address, + required int validUntil, + this.additionalInfo = const Value.absent(), + this.rowid = const Value.absent(), + }) : name = Value(name), + address = Value(address), + validUntil = Value(validUntil); + static Insertable custom({ + Expression? name, + Expression? address, + Expression? validUntil, + Expression? additionalInfo, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (name != null) 'name': name, + if (address != null) 'address': address, + if (validUntil != null) 'valid_until': validUntil, + if (additionalInfo != null) 'additional_info': additionalInfo, + if (rowid != null) 'rowid': rowid, + }); + } + + SparkNamesCompanion copyWith( + {Value? name, + Value? address, + Value? validUntil, + Value? additionalInfo, + Value? rowid}) { + return SparkNamesCompanion( + name: name ?? this.name, + address: address ?? this.address, + validUntil: validUntil ?? this.validUntil, + additionalInfo: additionalInfo ?? this.additionalInfo, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = Variable(name.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (validUntil.present) { + map['valid_until'] = Variable(validUntil.value); + } + if (additionalInfo.present) { + map['additional_info'] = Variable(additionalInfo.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('SparkNamesCompanion(') + ..write('name: $name, ') + ..write('address: $address, ') + ..write('validUntil: $validUntil, ') + ..write('additionalInfo: $additionalInfo, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$WalletDatabase extends GeneratedDatabase { + _$WalletDatabase(QueryExecutor e) : super(e); + $WalletDatabaseManager get managers => $WalletDatabaseManager(this); + late final $SparkNamesTable sparkNames = $SparkNamesTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [sparkNames]; +} + +typedef $$SparkNamesTableCreateCompanionBuilder = SparkNamesCompanion Function({ + required String name, + required String address, + required int validUntil, + Value additionalInfo, + Value rowid, +}); +typedef $$SparkNamesTableUpdateCompanionBuilder = SparkNamesCompanion Function({ + Value name, + Value address, + Value validUntil, + Value additionalInfo, + Value rowid, +}); + +class $$SparkNamesTableFilterComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnFilters(column)); + + ColumnFilters get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnFilters(column)); + + ColumnFilters get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => ColumnFilters(column)); + + ColumnFilters get additionalInfo => $composableBuilder( + column: $table.additionalInfo, + builder: (column) => ColumnFilters(column)); +} + +class $$SparkNamesTableOrderingComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get additionalInfo => $composableBuilder( + column: $table.additionalInfo, + builder: (column) => ColumnOrderings(column)); +} + +class $$SparkNamesTableAnnotationComposer + extends Composer<_$WalletDatabase, $SparkNamesTable> { + $$SparkNamesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get validUntil => $composableBuilder( + column: $table.validUntil, builder: (column) => column); + + GeneratedColumn get additionalInfo => $composableBuilder( + column: $table.additionalInfo, builder: (column) => column); +} + +class $$SparkNamesTableTableManager extends RootTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()> { + $$SparkNamesTableTableManager(_$WalletDatabase db, $SparkNamesTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$SparkNamesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$SparkNamesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$SparkNamesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value name = const Value.absent(), + Value address = const Value.absent(), + Value validUntil = const Value.absent(), + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + createCompanionCallback: ({ + required String name, + required String address, + required int validUntil, + Value additionalInfo = const Value.absent(), + Value rowid = const Value.absent(), + }) => + SparkNamesCompanion.insert( + name: name, + address: address, + validUntil: validUntil, + additionalInfo: additionalInfo, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$SparkNamesTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $SparkNamesTable, + SparkName, + $$SparkNamesTableFilterComposer, + $$SparkNamesTableOrderingComposer, + $$SparkNamesTableAnnotationComposer, + $$SparkNamesTableCreateCompanionBuilder, + $$SparkNamesTableUpdateCompanionBuilder, + (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), + SparkName, + PrefetchHooks Function()>; + +class $WalletDatabaseManager { + final _$WalletDatabase _db; + $WalletDatabaseManager(this._db); + $$SparkNamesTableTableManager get sparkNames => + $$SparkNamesTableTableManager(_db, _db.sparkNames); +} diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 2691adf5a..3852d9be9 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -93,11 +93,8 @@ class ElectrumXClient { // StreamChannel? get electrumAdapterChannel => _electrumAdapterChannel; StreamChannel? _electrumAdapterChannel; - ElectrumClient? getElectrumAdapter() => - ClientManager.sharedInstance.getClient( - cryptoCurrency: cryptoCurrency, - netType: netType, - ); + ElectrumClient? getElectrumAdapter() => ClientManager.sharedInstance + .getClient(cryptoCurrency: cryptoCurrency, netType: netType); late Prefs _prefs; late TorService _torService; @@ -109,12 +106,10 @@ class ElectrumXClient { // add finalizer to cancel stream subscription when all references to an // instance of ElectrumX becomes inaccessible - static final Finalizer _finalizer = Finalizer( - (p0) { - p0._torPreferenceListener?.cancel(); - p0._torStatusListener?.cancel(); - }, - ); + static final Finalizer _finalizer = Finalizer((p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }); StreamSubscription? _torPreferenceListener; StreamSubscription? _torStatusListener; @@ -129,8 +124,9 @@ class ElectrumXClient { required this.netType, required List failovers, required this.cryptoCurrency, - this.connectionTimeoutForSpecialCaseJsonRPCClients = - const Duration(seconds: 60), + this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration( + seconds: 60, + ), TorService? torService, EventBus? globalEventBusForTesting, }) { @@ -144,46 +140,45 @@ class ElectrumXClient { final bus = globalEventBusForTesting ?? GlobalEventBus.instance; // Listen for tor status changes. - _torStatusListener = bus.on().listen( - (event) async { - switch (event.newStatus) { - case TorConnectionStatus.connecting: - await _torConnectingLock.acquire(); - _requireMutex = true; - break; - - case TorConnectionStatus.connected: - case TorConnectionStatus.disconnected: - if (_torConnectingLock.isLocked) { - _torConnectingLock.release(); - } - _requireMutex = false; - break; - } - }, - ); + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + await _torConnectingLock.acquire(); + _requireMutex = true; + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + _requireMutex = false; + break; + } + }); // Listen for tor preference changes. - _torPreferenceListener = bus.on().listen( - (event) async { - // not sure if we need to do anything specific here - // switch (event.status) { - // case TorStatus.enabled: - // case TorStatus.disabled: - // } - - // setting to null should force the creation of a new json rpc client - // on the next request sent through this electrumx instance - _electrumAdapterChannel = null; - await (await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency)) - .$1 - ?.close(); - - // Also close any chain height services that are currently open. - // await ChainHeightServiceManager.dispose(); - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + // not sure if we need to do anything specific here + // switch (event.status) { + // case TorStatus.enabled: + // case TorStatus.disabled: + // } + + // setting to null should force the creation of a new json rpc client + // on the next request sent through this electrumx instance + _electrumAdapterChannel = null; + await (await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + )).$1?.close(); + + // Also close any chain height services that are currently open. + // await ChainHeightServiceManager.dispose(); + }); } factory ElectrumXClient.from({ @@ -252,14 +247,16 @@ class ElectrumXClient { if (netType == TorPlainNetworkOption.clear) { _electrumAdapterChannel = null; - await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency); + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); } } else { if (netType == TorPlainNetworkOption.tor) { _electrumAdapterChannel = null; - await ClientManager.sharedInstance - .remove(cryptoCurrency: cryptoCurrency); + await ClientManager.sharedInstance.remove( + cryptoCurrency: cryptoCurrency, + ); } } @@ -338,24 +335,22 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + await _torConnectingLock.protect( + () async => await checkElectrumAdapter(), + ); } else { await checkElectrumAdapter(); } try { - final response = await getElectrumAdapter()!.request( - command, - args, - ); + final response = await getElectrumAdapter()!.request(command, args); if (response is Map && response.keys.contains("error") && response["error"] != null) { - if (response["error"] - .toString() - .contains("No such mempool or blockchain transaction")) { + if (response["error"].toString().contains( + "No such mempool or blockchain transaction", + )) { throw NoSuchTransactionException( "No such mempool or blockchain transaction", args.first.toString(), @@ -399,11 +394,7 @@ class ElectrumXClient { } } catch (e, s) { final errorMessage = e.toString(); - Logging.instance.w( - "$host $e", - error: e, - stackTrace: s, - ); + Logging.instance.w("$host $e", error: e, stackTrace: s); if (errorMessage.contains("JSON-RPC error")) { currentFailoverIndex = _failovers.length; } @@ -437,8 +428,9 @@ class ElectrumXClient { } if (_requireMutex) { - await _torConnectingLock - .protect(() async => await checkElectrumAdapter()); + await _torConnectingLock.protect( + () async => await checkElectrumAdapter(), + ); } else { await checkElectrumAdapter(); } @@ -531,18 +523,19 @@ class ElectrumXClient { // 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: 30), - retries: retryCount, - ).timeout( - const Duration(seconds: 30), - onTimeout: () { - Logging.instance.d( - "ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host", - ); - }, - ) as bool; + requestID: requestID, + command: 'server.ping', + requestTimeout: const Duration(seconds: 30), + retries: retryCount, + ).timeout( + const Duration(seconds: 30), + onTimeout: () { + Logging.instance.d( + "ElectrumxClient.ping timed out with retryCount=$retryCount, host=$_host", + ); + }, + ) + as bool; } catch (e) { rethrow; } @@ -609,9 +602,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.transaction.broadcast', - args: [ - rawTx, - ], + args: [rawTx], ); return response as String; } catch (e) { @@ -636,9 +627,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.scripthash.get_balance', - args: [ - scripthash, - ], + args: [scripthash], ); return Map.from(response as Map); } catch (e) { @@ -673,9 +662,7 @@ class ElectrumXClient { requestID: requestID, command: 'blockchain.scripthash.get_history', requestTimeout: const Duration(minutes: 5), - args: [ - scripthash, - ], + args: [scripthash], ); result = response; retryCount--; @@ -731,9 +718,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.scripthash.listunspent', - args: [ - scripthash, - ], + args: [scripthash], ); return List>.from(response as List); } catch (e) { @@ -826,14 +811,10 @@ class ElectrumXClient { bool verbose = true, String? requestID, }) async { - Logging.instance.d( - "attempting to fetch blockchain.transaction.get...", - ); + Logging.instance.d("attempting to fetch blockchain.transaction.get..."); await checkElectrumAdapter(); final dynamic response = await getElectrumAdapter()!.getTransaction(txHash); - Logging.instance.d( - "Fetching blockchain.transaction.get finished", - ); + Logging.instance.d("Fetching blockchain.transaction.get finished"); if (!verbose) { return {"rawtx": response as String}; @@ -861,16 +842,12 @@ class ElectrumXClient { String blockhash = "", String? requestID, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getanonymityset...", - ); + Logging.instance.d("attempting to fetch lelantus.getanonymityset..."); await checkElectrumAdapter(); - final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); - Logging.instance.d( - "Fetching lelantus.getanonymityset finished", - ); + final Map response = await (getElectrumAdapter() + as FiroElectrumClient) + .getLelantusAnonymitySet(groupId: groupId, blockHash: blockhash); + Logging.instance.d("Fetching lelantus.getanonymityset finished"); return response; } @@ -882,15 +859,11 @@ class ElectrumXClient { dynamic mints, String? requestID, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getmintmetadata...", - ); + Logging.instance.d("attempting to fetch lelantus.getmintmetadata..."); await checkElectrumAdapter(); final dynamic response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusMintData(mints: mints); - Logging.instance.d( - "Fetching lelantus.getmintmetadata finished", - ); + Logging.instance.d("Fetching lelantus.getmintmetadata finished"); return response; } @@ -900,9 +873,7 @@ class ElectrumXClient { String? requestID, required int startNumber, }) async { - Logging.instance.d( - "attempting to fetch lelantus.getusedcoinserials...", - ); + Logging.instance.d("attempting to fetch lelantus.getusedcoinserials..."); await checkElectrumAdapter(); int retryCount = 3; @@ -912,9 +883,7 @@ class ElectrumXClient { response = await (getElectrumAdapter() as FiroElectrumClient) .getLelantusUsedCoinSerials(startNumber: startNumber); // TODO add 2 minute timeout. - Logging.instance.d( - "Fetching lelantus.getusedcoinserials finished", - ); + Logging.instance.d("Fetching lelantus.getusedcoinserials finished"); retryCount--; } @@ -926,15 +895,11 @@ class ElectrumXClient { /// /// ex: 1 Future getLelantusLatestCoinId({String? requestID}) async { - Logging.instance.d( - "attempting to fetch lelantus.getlatestcoinid...", - ); + Logging.instance.d("attempting to fetch lelantus.getlatestcoinid..."); await checkElectrumAdapter(); final int response = await (getElectrumAdapter() as FiroElectrumClient).getLatestCoinId(); - Logging.instance.d( - "Fetching lelantus.getlatestcoinid finished", - ); + Logging.instance.d("Fetching lelantus.getlatestcoinid finished"); return response; } @@ -961,12 +926,12 @@ class ElectrumXClient { try { final start = DateTime.now(); await checkElectrumAdapter(); - final Map response = - await (getElectrumAdapter() as FiroElectrumClient) - .getSparkAnonymitySet( - coinGroupId: coinGroupId, - startBlockHash: startBlockHash, - ); + final Map response = await (getElectrumAdapter() + as FiroElectrumClient) + .getSparkAnonymitySet( + coinGroupId: coinGroupId, + startBlockHash: startBlockHash, + ); Logging.instance.d( "Finished ElectrumXClient.getSparkAnonymitySet(coinGroupId" "=$coinGroupId, startBlockHash=$startBlockHash). " @@ -1053,34 +1018,23 @@ class ElectrumXClient { /// Returns the latest Spark set id /// /// ex: 1 - Future getSparkLatestCoinId({ - String? requestID, - }) async { + Future getSparkLatestCoinId({String? requestID}) async { try { - Logging.instance.d( - "attempting to fetch spark.getsparklatestcoinid...", - ); + Logging.instance.d("attempting to fetch spark.getsparklatestcoinid..."); await checkElectrumAdapter(); - final int response = await (getElectrumAdapter() as FiroElectrumClient) - .getSparkLatestCoinId(); - Logging.instance.d( - "Fetching spark.getsparklatestcoinid finished", - ); + final int response = + await (getElectrumAdapter() as FiroElectrumClient) + .getSparkLatestCoinId(); + Logging.instance.d("Fetching spark.getsparklatestcoinid finished"); return response; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } /// Returns the txids of the current transactions found in the mempool - Future> getMempoolTxids({ - String? requestID, - }) async { + Future> getMempoolTxids({String? requestID}) async { try { final start = DateTime.now(); final response = await request( @@ -1088,9 +1042,10 @@ class ElectrumXClient { command: "spark.getmempoolsparktxids", ); - final txids = List.from(response as List) - .map((e) => e.toHexReversedFromBase64) - .toSet(); + final txids = + List.from( + response as List, + ).map((e) => e.toHexReversedFromBase64).toSet(); Logging.instance.d( "Finished ElectrumXClient.getMempoolTxids(). " @@ -1099,11 +1054,7 @@ class ElectrumXClient { return txids; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1119,9 +1070,7 @@ class ElectrumXClient { requestID: requestID, command: "spark.getmempoolsparktxs", args: [ - { - "txids": txids, - }, + {"txids": txids}, ], ); @@ -1131,8 +1080,9 @@ class ElectrumXClient { result.add( SparkMempoolData( txid: entry.key, - serialContext: - List.from(entry.value["serial_context"] as List), + serialContext: List.from( + entry.value["serial_context"] as List, + ), // the space after lTags is required lol lTags: List.from(entry.value["lTags "] as List), coins: List.from(entry.value["coins"] as List), @@ -1163,9 +1113,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: "spark.getusedcoinstagstxhashes", - args: [ - "$startNumber", - ], + args: ["$startNumber"], ); final map = Map.from(response as Map); @@ -1179,14 +1127,85 @@ class ElectrumXClient { return tags; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, + Logging.instance.e(e, error: e, stackTrace: s); + rethrow; + } + } + + Future> getSparkNames({ + String? requestID, + }) async { + try { + final start = DateTime.now(); + await checkElectrumAdapter(); + const command = "spark.getsparknames"; + Logging.instance.d( + "[${getElectrumAdapter()?.host}] => attempting to fetch $command...", ); + + final response = await request(requestID: requestID, command: command); + + if (response is List) { + Logging.instance.d( + "Finished ElectrumXClient.getSparkNames(). " + "names.length: ${response.length}" + "Duration=${DateTime.now().difference(start)}", + ); + + return response + .map( + (e) => ( + name: e["name"] as String, + address: e["address"] as String, + ), + ) + .toList(); + } else if (response["error"] != null) { + Logging.instance.d(response); + throw Exception(response["error"].toString()); + } else { + throw Exception("Failed to parse getSparkNames response: $response"); + } + } catch (e) { rethrow; } } + + Future<({String address, int validUntil, String additionalInfo})> + getSparkNameData({required String sparkName, String? requestID}) async { + try { + final start = DateTime.now(); + await checkElectrumAdapter(); + const command = "spark.getsparknamedata"; + Logging.instance.d( + "[${getElectrumAdapter()?.host}] => attempting to fetch $command...", + ); + + final response = await request( + requestID: requestID, + command: command, + args: [sparkName], + ); + + Logging.instance.d( + "Finished ElectrumXClient.getSparkNameData(). " + "Duration=${DateTime.now().difference(start)}", + ); + if (response["error"] != null) { + Logging.instance.d(response); + throw Exception(response["error"].toString()); + } + + return ( + address: response["address"] as String, + validUntil: response["validUntil"] as int, + additionalInfo: response["additionalInfo"] as String, + ); + } catch (e) { + rethrow; + } + } + // ======== New Paginated Endpoints ========================================== Future getSparkAnonymitySetMeta({ @@ -1203,9 +1222,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: command, - args: [ - "$coinGroupId", - ], + args: ["$coinGroupId"], ); final map = Map.from(response as Map); @@ -1227,11 +1244,7 @@ class ElectrumXClient { return result; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1250,12 +1263,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: command, - args: [ - "$coinGroupId", - latestBlock, - "$startIndex", - "$endIndex", - ], + args: ["$coinGroupId", latestBlock, "$startIndex", "$endIndex"], ); final map = Map.from(response as Map); @@ -1275,11 +1283,7 @@ class ElectrumXClient { return result; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1296,10 +1300,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: "blockchain.checkifmncollateral", - args: [ - txid, - index.toString(), - ], + args: [txid, index.toString()], ); Logging.instance.d( @@ -1310,11 +1311,7 @@ class ElectrumXClient { return response as bool; } catch (e, s) { - Logging.instance.e( - e, - error: e, - stackTrace: s, - ); + Logging.instance.e(e, error: e, stackTrace: s); rethrow; } } @@ -1344,9 +1341,7 @@ class ElectrumXClient { final response = await request( requestID: requestID, command: 'blockchain.estimatefee', - args: [ - blocks, - ], + args: [blocks], ); try { if (response == null || @@ -1371,7 +1366,8 @@ class ElectrumXClient { } return Decimal.parse(response.toString()); } catch (e, s) { - final String msg = "Error parsing fee rate. Response: $response" + final String msg = + "Error parsing fee rate. Response: $response" "\nResult: $response\nError: $e\nStack trace: $s"; Logging.instance.e(msg, error: e, stackTrace: s); throw Exception(msg); diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 7b867aab3..be9c3ac37 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -86,8 +86,9 @@ class _Step4ViewState extends ConsumerState { } Future _updateStatus() async { - final statusResponse = - await ref.read(efExchangeProvider).updateTrade(model.trade!); + final statusResponse = await ref + .read(efExchangeProvider) + .updateTrade(model.trade!); String status = "Waiting"; if (statusResponse.value != null) { status = statusResponse.value!.status; @@ -149,46 +150,34 @@ class _Step4ViewState extends ConsumerState { Theme.of(context).extension()!.backgroundAppBar, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius * 3, - ), + top: Radius.circular(Constants.size.circularBorderRadius * 3), ), ), builder: (context) { return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Text( "Select Firo balance", style: STextStyles.pageTitleH2(context), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), SecondaryButton( label: "${ref.watch(pAmountFormatter(coin)).format(balancePrivate.spendable)} (private)", onPressed: () => Navigator.of(context).pop(false), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), SecondaryButton( label: "${ref.watch(pAmountFormatter(coin)).format(balancePublic.spendable)} (public)", onPressed: () => Navigator.of(context).pop(true), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), ], ), ); @@ -237,55 +226,39 @@ class _Step4ViewState extends ConsumerState { ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); Future txDataFuture; if (wallet is FiroWallet && !firoPublicSend) { txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], - note: "${model.trade!.payInCurrency.toUpperCase()}/" + recipients: [(address: address, amount: amount, isChange: false)], + note: + "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", ), ); } else { - final memo = wallet.info.coin is Stellar - ? model.trade!.payInExtraId.isNotEmpty - ? model.trade!.payInExtraId - : null - : null; + final memo = + wallet.info.coin is Stellar + ? model.trade!.payInExtraId.isNotEmpty + ? model.trade!.payInExtraId + : null + : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: address, amount: amount, isChange: false)], memo: memo, feeRateType: FeeRateType.average, - note: "${model.trade!.payInCurrency.toUpperCase()}/" + note: + "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); final txData = results.first as TxData; @@ -301,13 +274,14 @@ class _Step4ViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - txData: txData, - walletId: tuple.item1, - routeOnSuccessName: HomeView.routeName, - trade: model.trade!, - shouldSendPublicFiroFunds: firoPublicSend, - ), + builder: + (_) => ConfirmChangeNowSendView( + txData: txData, + walletId: tuple.item1, + routeOnSuccessName: HomeView.routeName, + trade: model.trade!, + shouldSendPublicFiroFunds: firoPublicSend, + ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, ), @@ -338,9 +312,10 @@ class _Step4ViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -357,8 +332,10 @@ class _Step4ViewState extends ConsumerState { @override Widget build(BuildContext context) { - final bool isWalletCoin = - _isWalletCoinAndHasWallet(model.trade!.payInCurrency, ref); + final bool isWalletCoin = _isWalletCoinAndHasWallet( + model.trade!.payInCurrency, + ref, + ); return WillPopScope( onWillPop: () async { await _close(); @@ -379,17 +356,15 @@ class _Step4ViewState extends ConsumerState { Assets.svg.x, width: 24, height: 24, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: _close, ), ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), body: LayoutBuilder( builder: (context, constraints) { @@ -407,59 +382,51 @@ class _Step4ViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - StepRow( - count: 4, - current: 3, - width: width, - ), - const SizedBox( - height: 14, - ), + StepRow(count: 4, current: 3, width: width), + const SizedBox(height: 14), Text( "Send ${model.sendTicker.toUpperCase()} to the address below", style: STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "Send ${model.sendTicker.toUpperCase()} to the address below. Once it is received, ${model.trade!.exchangeName} will send the ${model.receiveTicker.toUpperCase()} to the recipient address you provided. You can find this trade details and check its status in the list of trades.", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, + color: + Theme.of( + context, + ).extension()!.warningBackground, child: RichText( text: TextSpan( text: "You must send at least ${model.sendAmount.toString()} ${model.sendTicker}. ", style: STextStyles.label700(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + color: + Theme.of(context) + .extension()! + .warningForeground, ), children: [ TextSpan( text: "If you send less than ${model.sendAmount.toString()} ${model.sendTicker}, your transaction may not be converted and it may not be refunded.", - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, ), ), ], ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -470,8 +437,9 @@ class _Step4ViewState extends ConsumerState { children: [ Text( "Amount", - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), GestureDetector( onTap: () async { @@ -493,14 +461,13 @@ class _Step4ViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of(context) + .extension()! + .infoItemIcons, width: 10, ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), Text( "Copy", style: STextStyles.link2(context), @@ -510,9 +477,7 @@ class _Step4ViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( "${model.sendAmount.toString()} ${model.sendTicker.toUpperCase()}", style: STextStyles.itemSubtitle12(context), @@ -520,9 +485,7 @@ class _Step4ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -533,8 +496,9 @@ class _Step4ViewState extends ConsumerState { children: [ Text( "Send ${model.sendTicker.toUpperCase()} to this address", - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), GestureDetector( onTap: () async { @@ -556,14 +520,13 @@ class _Step4ViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of(context) + .extension()! + .infoItemIcons, width: 10, ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), Text( "Copy", style: STextStyles.link2(context), @@ -573,9 +536,7 @@ class _Step4ViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( model.trade!.payInAddress, style: STextStyles.itemSubtitle12(context), @@ -583,9 +544,7 @@ class _Step4ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), if (model.trade!.payInExtraId.isNotEmpty) RoundedWhiteContainer( child: Column( @@ -597,8 +556,9 @@ class _Step4ViewState extends ConsumerState { children: [ Text( "Memo", - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), GestureDetector( onTap: () async { @@ -621,39 +581,38 @@ class _Step4ViewState extends ConsumerState { children: [ SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, width: 10, ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), Text( "Copy", - style: - STextStyles.link2(context), + style: STextStyles.link2( + context, + ), ), ], ), ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( model.trade!.payInExtraId, - style: - STextStyles.itemSubtitle12(context), + style: STextStyles.itemSubtitle12( + context, + ), ), ], ), ), if (model.trade!.payInExtraId.isNotEmpty) - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), RoundedWhiteContainer( child: Row( children: [ @@ -666,12 +625,11 @@ class _Step4ViewState extends ConsumerState { children: [ Text( model.trade!.tradeId, - style: - STextStyles.itemSubtitle12(context), - ), - const SizedBox( - width: 10, + style: STextStyles.itemSubtitle12( + context, + ), ), + const SizedBox(width: 10), GestureDetector( onTap: () async { final data = ClipboardData( @@ -690,9 +648,10 @@ class _Step4ViewState extends ConsumerState { }, child: SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of(context) + .extension()! + .infoItemIcons, width: 12, ), ), @@ -701,9 +660,7 @@ class _Step4ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), RoundedWhiteContainer( child: Row( mainAxisAlignment: @@ -715,8 +672,9 @@ class _Step4ViewState extends ConsumerState { ), Text( _statusString, - style: STextStyles.itemSubtitle(context) - .copyWith( + style: STextStyles.itemSubtitle( + context, + ).copyWith( color: Theme.of(context) .extension()! .colorForStatus(_statusString), @@ -726,9 +684,7 @@ class _Step4ViewState extends ConsumerState { ), ), const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), TextButton( onPressed: () { showDialog( @@ -738,9 +694,7 @@ class _Step4ViewState extends ConsumerState { return StackDialogBase( child: Column( children: [ - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Center( child: Text( "Send ${model.sendTicker} to this address", @@ -749,31 +703,30 @@ class _Step4ViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Center( child: QR( // TODO: grab coin uri scheme from somewhere // data: "${coin.uriScheme}:$receivingAddress", data: model.trade!.payInAddress, - size: MediaQuery.of(context) - .size - .width / + size: + MediaQuery.of( + context, + ).size.width / 2, ), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), Row( children: [ const Spacer(), Expanded( child: TextButton( - onPressed: () => - Navigator.of(context) - .pop(), + onPressed: + () => + Navigator.of( + context, + ).pop(), style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( @@ -784,10 +737,12 @@ class _Step4ViewState extends ConsumerState { style: STextStyles.button( context, ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .buttonTextSecondary, + color: + Theme.of(context) + .extension< + StackColors + >()! + .buttonTextSecondary, ), ), ), @@ -808,76 +763,83 @@ class _Step4ViewState extends ConsumerState { style: STextStyles.button(context), ), ), - if (isWalletCoin) - const SizedBox( - height: 12, - ), + if (isWalletCoin) const SizedBox(height: 12), if (isWalletCoin) Builder( builder: (context) { String buttonTitle = "Send from ${AppConfig.appName}"; - final tuple = ref - .read( - exchangeSendFromWalletIdStateProvider - .state, - ) - .state; + final tuple = + ref + .read( + exchangeSendFromWalletIdStateProvider + .state, + ) + .state; if (tuple != null && model.sendTicker.toLowerCase() == tuple.item2.ticker.toLowerCase()) { - final walletName = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .name; + final walletName = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .name; buttonTitle = "Send from $walletName"; } return TextButton( - onPressed: tuple != null && - model.sendTicker.toLowerCase() == - tuple.item2.ticker.toLowerCase() - ? () async { - await _confirmSend(tuple); - } - : () { - Navigator.of(context).push( - RouteGenerator.getRoute( - shouldUseMaterialRoute: - RouteGenerator - .useMaterialPageRoute, - builder: - (BuildContext context) { - final coin = AppConfig.coins - .firstWhere( - (e) => - e.ticker - .toLowerCase() == - model.trade! - .payInCurrency - .toLowerCase(), - ); - - return SendFromView( - coin: coin, - amount: model.sendAmount - .toAmount( - fractionDigits: - coin.fractionDigits, - ), - address: model - .trade!.payInAddress, - trade: model.trade!, - ); - }, - settings: const RouteSettings( - name: SendFromView.routeName, + onPressed: + tuple != null && + model.sendTicker + .toLowerCase() == + tuple.item2.ticker + .toLowerCase() + ? () async { + await _confirmSend(tuple); + } + : () { + Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator + .useMaterialPageRoute, + builder: ( + BuildContext context, + ) { + final coin = AppConfig.coins + .firstWhere( + (e) => + e.ticker + .toLowerCase() == + model + .trade! + .payInCurrency + .toLowerCase(), + ); + + return SendFromView( + coin: coin, + amount: model.sendAmount + .toAmount( + fractionDigits: + coin.fractionDigits, + ), + address: + model + .trade! + .payInAddress, + trade: model.trade!, + ); + }, + settings: const RouteSettings( + name: + SendFromView.routeName, + ), ), - ), - ); - }, + ); + }, style: Theme.of(context) .extension()! .getSecondaryEnabledButtonStyle( @@ -885,11 +847,13 @@ class _Step4ViewState extends ConsumerState { ), child: Text( buttonTitle, - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), ); diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 7e97017a3..850623031 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -91,12 +91,13 @@ class _SendFromViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final walletIds = ref - .watch(pWallets) - .wallets - .where((e) => e.info.coin == coin) - .map((e) => e.walletId) - .toList(); + final walletIds = + ref + .watch(pWallets) + .wallets + .where((e) => e.info.coin == coin) + .map((e) => e.walletId) + .toList(); final isDesktop = Util.isDesktop; @@ -113,55 +114,49 @@ class _SendFromViewState extends ConsumerState { Navigator.of(context).pop(); }, ), - title: Text( - "Send from", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: child, + title: Text("Send from", style: STextStyles.navBarTitle(context)), ), + body: Padding(padding: const EdgeInsets.all(16), child: child), ), ); }, child: ConditionalParent( condition: isDesktop, - builder: (child) => DesktopDialog( - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (child) => DesktopDialog( + maxHeight: double.infinity, + child: Column( children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Send from ${AppConfig.prefix}", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: widget.shouldPopRoot, + ).pop, + ), + ], + ), Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Send from ${AppConfig.prefix}", - style: STextStyles.desktopH3(context), - ), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: widget.shouldPopRoot, - ).pop, + child: child, ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: child, - ), - ], - ), - ), + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -169,20 +164,17 @@ class _SendFromViewState extends ConsumerState { children: [ Text( "You need to send ${ref.watch(pAmountFormatter(coin)).format(amount)}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ListView.builder( primary: isDesktop ? false : null, shrinkWrap: isDesktop, @@ -250,14 +242,15 @@ class _SendFromCardState extends ConsumerState { builder: (context) { return ConditionalParent( condition: Util.isDesktop, - builder: (child) => DesktopDialog( - maxWidth: 400, - maxHeight: double.infinity, - child: Padding( - padding: const EdgeInsets.all(32), - child: child, - ), - ), + builder: + (child) => DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: child, + ), + ), child: BuildingTransactionDialog( coin: coin, isSpark: @@ -282,31 +275,22 @@ class _SendFromCardState extends ConsumerState { await wallet.open(); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { - final memo = coin is Stellar - ? trade.payInExtraId.isNotEmpty - ? trade.payInExtraId - : null - : null; + final memo = + coin is Stellar + ? trade.payInExtraId.isNotEmpty + ? trade.payInExtraId + : null + : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: address, amount: amount, isChange: false)], memo: memo, feeRateType: FeeRateType.average, ), @@ -317,36 +301,21 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: address, amount: amount, isChange: false)], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( - recipients: [ - ( - address: address, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: address, amount: amount, isChange: false)], // feeRateType: FeeRateType.average, ), ); } } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -354,14 +323,12 @@ class _SendFromCardState extends ConsumerState { // pop building dialog if (mounted) { - Navigator.of( - context, - rootNavigator: Util.isDesktop, - ).pop(); + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); } txData = txData.copyWith( - note: "${trade.payInCurrency.toUpperCase()}/" + note: + "${trade.payInCurrency.toUpperCase()}/" "${trade.payOutCurrency.toUpperCase()} exchange", ); @@ -369,16 +336,18 @@ class _SendFromCardState extends ConsumerState { await Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmChangeNowSendView( - txData: txData, - walletId: walletId, - routeOnSuccessName: Util.isDesktop - ? DesktopExchangeView.routeName - : HomeView.routeName, - trade: trade, - shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, - fromDesktopStep4: widget.fromDesktopStep4, - ), + builder: + (_) => ConfirmChangeNowSendView( + txData: txData, + walletId: walletId, + routeOnSuccessName: + Util.isDesktop + ? DesktopExchangeView.routeName + : HomeView.routeName, + trade: trade, + shouldSendPublicFiroFunds: shouldSendPublicFiroFunds, + fromDesktopStep4: widget.fromDesktopStep4, + ), settings: const RouteSettings( name: ConfirmChangeNowSendView.routeName, ), @@ -407,9 +376,10 @@ class _SendFromCardState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, ), ), onPressed: () { @@ -448,86 +418,154 @@ class _SendFromCardState extends ConsumerState { padding: const EdgeInsets.all(0), child: ConditionalParent( condition: isFiro, - builder: (child) => Expandable( - header: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.all(12), - child: child, - ), - ), - body: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MaterialButton( - splashColor: - Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - onPressed: () async { - if (mounted) { - unawaited( - _send( - shouldSendPublicFiroFunds: false, + builder: + (child) => Expandable( + header: Container( + color: Colors.transparent, + child: Padding(padding: const EdgeInsets.all(12), child: child), + ), + body: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonFiroPrivateKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - ); - } - }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 6, - left: 16, - right: 16, - bottom: 6, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + onPressed: () async { + if (mounted) { + unawaited(_send(shouldSendPublicFiroFunds: false)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Use private balance", - style: STextStyles.itemSubtitle(context), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use private balance", + style: STextStyles.itemSubtitle(context), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch( + pWalletBalanceTertiary(walletId), + ) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + ), + ], ), - Text( - ref.watch(pAmountFormatter(coin)).format( - ref - .watch(pWalletBalanceTertiary(walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle(context), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: + Theme.of( + context, + ).extension()!.infoItemLabel, ), ], ), - SvgPicture.asset( - Assets.svg.chevronRight, - height: 14, - width: 7, - color: Theme.of(context) - .extension()! - .infoItemLabel, + ), + ), + ), + MaterialButton( + splashColor: + Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), + padding: const EdgeInsets.all(0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () async { + if (mounted) { + unawaited(_send(shouldSendPublicFiroFunds: true)); + } + }, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 6, + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Use public balance", + style: STextStyles.itemSubtitle(context), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + ref + .watch(pWalletBalance(walletId)) + .spendable, + ), + style: STextStyles.itemSubtitle(context), + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronRight, + height: 14, + width: 7, + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ], ), - ], + ), ), ), - ), + const SizedBox(height: 6), + ], ), - MaterialButton( + ), + child: ConditionalParent( + condition: !isFiro, + builder: + (child) => MaterialButton( splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonFiroPublicKey_$walletId"), - padding: const EdgeInsets.all(0), + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(8), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -536,83 +574,11 @@ class _SendFromCardState extends ConsumerState { ), onPressed: () async { if (mounted) { - unawaited( - _send( - shouldSendPublicFiroFunds: true, - ), - ); + unawaited(_send()); } }, - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.only( - top: 6, - left: 16, - right: 16, - bottom: 6, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Use public balance", - style: STextStyles.itemSubtitle(context), - ), - Text( - ref.watch(pAmountFormatter(coin)).format( - ref - .watch(pWalletBalance(walletId)) - .spendable, - ), - style: STextStyles.itemSubtitle(context), - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronRight, - height: 14, - width: 7, - color: Theme.of(context) - .extension()! - .infoItemLabel, - ), - ], - ), - ), - ), - ), - const SizedBox( - height: 6, - ), - ], - ), - ), - child: ConditionalParent( - condition: !isFiro, - builder: (child) => MaterialButton( - splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(8), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + child: child, ), - ), - onPressed: () async { - if (mounted) { - unawaited( - _send(), - ); - } - }, - child: child, - ), child: Row( children: [ Container( @@ -625,19 +591,13 @@ class _SendFromCardState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(6), child: SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -647,13 +607,12 @@ class _SendFromCardState extends ConsumerState { ref.watch(pWalletName(walletId)), style: STextStyles.titleBold12(context), ), - if (!isFiro) - const SizedBox( - height: 2, - ), + if (!isFiro) const SizedBox(height: 2), if (!isFiro) Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref.watch(pWalletBalance(walletId)).spendable, ), style: STextStyles.itemSubtitle(context), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 4bdcef6e2..57a78ad08 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -27,6 +27,7 @@ import '../../providers/ui/fee_rate_type_state_provider.dart'; import '../../providers/ui/preview_tx_button_state_provider.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../route_generator.dart'; +import '../../services/spark_names_service.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/address_utils.dart'; @@ -150,13 +151,12 @@ class _SendViewState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: coin.fractionDigits, - ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: coin.fractionDigits); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); ref.read(pSendAmount.notifier).state = amount; } @@ -173,6 +173,82 @@ class _SendViewState extends ConsumerState { } } + Future _checkSparkNameAndOrSetAddress( + String content, { + bool setController = true, + }) async { + void setContent() { + if (setController) { + sendToController.text = content; + } + _address = content; + } + + // check for spark name + if (coin is Firo) { + final address = await SparkNamesService.getAddressFor( + content, + network: coin.network, + ); + if (address != null) { + // found a spark name + sendToController.text = content; + _address = address; + } else { + setContent(); + } + } else { + setContent(); + } + } + + Future _pasteAddress() async { + final ClipboardData? data = await clipboard.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")).trim(); + } + + try { + final paymentData = AddressUtils.parsePaymentUri( + content, + logging: Logging.instance, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { + _applyUri(paymentData); + } else { + if (coin is Epiccash) { + content = AddressUtils().formatAddress(content); + } + + sendToController.text = content; + _address = content; + + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } catch (e) { + if (coin is Epiccash) { + // strip http:// and https:// if content contains @ + content = AddressUtils().formatAddress(content); + } + + await _checkSparkNameAndOrSetAddress(content); + + // Trigger validation after pasting. + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + } + Future _scanQr() async { try { // ref @@ -244,23 +320,21 @@ class _SendViewState extends ConsumerState { if (_price == Decimal.zero) { amount = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits); } else { - amount = baseAmount <= Amount.zero - ? 0.toAmountAsRaw(fractionDigits: coin.fractionDigits) - : (baseAmount.decimal / _price) - .toDecimal( - scaleOnInfinitePrecision: coin.fractionDigits, - ) - .toAmount(fractionDigits: coin.fractionDigits); + amount = + baseAmount <= Amount.zero + ? 0.toAmountAsRaw(fractionDigits: coin.fractionDigits) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) + .toAmount(fractionDigits: coin.fractionDigits); } if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } _cachedAmountToSend = amount; - final amountString = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final amountString = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; @@ -281,9 +355,9 @@ class _SendViewState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text); final Amount? amount; if (cryptoAmount != null) { amount = cryptoAmount; @@ -297,9 +371,7 @@ class _SendViewState extends ConsumerState { if (price > Decimal.zero) { baseAmountController.text = (amount.decimal * price) - .toAmount( - fractionDigits: 2, - ) + .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, ); @@ -356,10 +428,12 @@ class _SendViewState extends ConsumerState { fee = fee.split(" ").first; } - final value = fee.contains(",") - ? Decimal.parse(fee.replaceFirst(",", ".")) - .toAmount(fractionDigits: coin.fractionDigits) - : Decimal.parse(fee).toAmount(fractionDigits: coin.fractionDigits); + final value = + fee.contains(",") + ? Decimal.parse( + fee.replaceFirst(",", "."), + ).toAmount(fractionDigits: coin.fractionDigits) + : Decimal.parse(fee).toAmount(fractionDigits: coin.fractionDigits); if (shouldSetState) { setState(() => _currentFee = value); @@ -368,36 +442,21 @@ class _SendViewState extends ConsumerState { } } - String? _updateInvalidAddressText(String address) { - if (_data != null && _data.contactLabel == address) { - return null; - } - - if (address.isNotEmpty && - !ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .validateAddress(address)) { - return "Invalid address"; - } - return null; - } - void _setValidAddressProviders(String? address) { if (isPaynymSend) { ref.read(pValidSendToAddress.notifier).state = true; } else { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - ref.read(pValidSparkSendToAddress.notifier).state = - SparkInterface.validateSparkAddress( + ref + .read(pValidSparkSendToAddress.notifier) + .state = SparkInterface.validateSparkAddress( address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); - ref.read(pIsExchangeAddress.state).state = - (coin as Firo).isExchangeAddress(address ?? ""); + ref.read(pIsExchangeAddress.state).state = (coin as Firo) + .isExchangeAddress(address ?? ""); if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && ref.read(pIsExchangeAddress) && @@ -410,8 +469,8 @@ class _SendViewState extends ConsumerState { } } - ref.read(pValidSendToAddress.notifier).state = - wallet.cryptoCurrency.validateAddress(address ?? ""); + ref.read(pValidSendToAddress.notifier).state = wallet.cryptoCurrency + .validateAddress(address ?? ""); } } @@ -486,11 +545,9 @@ class _SendViewState extends ConsumerState { } fee = await wallet.estimateFeeFor(amount, specialMoneroId.value); - cachedFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees[amount]!; } else if (isFiro) { @@ -499,39 +556,29 @@ class _SendViewState extends ConsumerState { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.public: fee = await firoWallet.estimateFeeFor(amount, feeRate); - cachedFiroPublicFees[amount] = - ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFiroPublicFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroPublicFees[amount]!; case FiroType.lelantus: fee = await firoWallet.estimateFeeForLelantus(amount); - cachedFiroLelantusFees[amount] = - ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFiroLelantusFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroLelantusFees[amount]!; case FiroType.spark: fee = await firoWallet.estimateFeeForSpark(amount); - cachedFiroSparkFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFiroSparkFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroSparkFees[amount]!; } } else { fee = await wallet.estimateFeeFor(amount, feeRate); - cachedFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees[amount]!; } @@ -540,9 +587,7 @@ class _SendViewState extends ConsumerState { Future _previewTransaction() async { // wait for keyboard to disappear FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + await Future.delayed(const Duration(milliseconds: 100)); final wallet = ref.read(pWallets).getWallet(walletId); final Amount amount = ref.read(pSendAmount)!; @@ -591,9 +636,10 @@ class _SendViewState extends ConsumerState { child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -604,10 +650,7 @@ class _SendViewState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Yes", - style: STextStyles.button(context), - ), + child: Text("Yes", style: STextStyles.button(context)), onPressed: () { Navigator.of(context).pop(true); }, @@ -636,7 +679,8 @@ class _SendViewState extends ConsumerState { builder: (context) { return BuildingTransactionDialog( coin: wallet.info.coin, - isSpark: wallet is FiroWallet && + isSpark: + wallet is FiroWallet && ref.read(publicPrivateBalanceStateProvider.state).state == FiroType.spark, onCancel: () { @@ -650,11 +694,7 @@ class _SendViewState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); Future txDataFuture; @@ -672,11 +712,12 @@ class _SendViewState extends ConsumerState { ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } else if (wallet is FiroWallet) { @@ -695,26 +736,24 @@ class _SendViewState extends ConsumerState { ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + utxos: + (coinControlEnabled && selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } else { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), + (address: _address!, amount: amount, isChange: false), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + utxos: + (coinControlEnabled && selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } @@ -724,11 +763,7 @@ class _SendViewState extends ConsumerState { txDataFuture = wallet.prepareSendLelantus( txData: TxData( recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), + (address: _address!, amount: amount, isChange: false), ], ), ); @@ -737,25 +772,23 @@ class _SendViewState extends ConsumerState { case FiroType.spark: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: ref.read(pValidSparkSendToAddress) - ? null - : [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - sparkRecipients: ref.read(pValidSparkSendToAddress) - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - isChange: false, - ), - ] - : null, + recipients: + ref.read(pValidSparkSendToAddress) + ? null + : [ + (address: _address!, amount: amount, isChange: false), + ], + sparkRecipients: + ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + isChange: false, + ), + ] + : null, ), ); break; @@ -764,29 +797,21 @@ class _SendViewState extends ConsumerState { final memo = coin is Stellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: _address!, amount: amount, isChange: false)], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); TxData txData = results.first as TxData; @@ -794,9 +819,10 @@ class _SendViewState extends ConsumerState { if (isPaynymSend) { txData = txData.copyWith( paynymAccountLite: widget.accountLite!, - note: noteController.text.isNotEmpty - ? noteController.text - : "PayNym send", + note: + noteController.text.isNotEmpty + ? noteController.text + : "PayNym send", ); } else { txData = txData.copyWith(note: noteController.text); @@ -810,12 +836,13 @@ class _SendViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isPaynymTransaction: isPaynymSend, - onSuccess: clearSendForm, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isPaynymTransaction: isPaynymSend, + onSuccess: clearSendForm, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -845,9 +872,10 @@ class _SendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -886,10 +914,9 @@ class _SendViewState extends ConsumerState { } Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: - utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), - fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, - ); + rawValue: utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), + fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + ); Future _sendAllTapped(bool showCoinControl) async { final Amount amount; @@ -912,10 +939,9 @@ class _SendViewState extends ConsumerState { amount = ref.read(pWalletBalance(walletId)).spendable; } - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChanged(); } @@ -934,8 +960,9 @@ class _SendViewState extends ConsumerState { }); _currentFee = 0.toAmountAsRaw(fractionDigits: coin.fractionDigits); - _calculateFeesFuture = - calculateFees(0.toAmountAsRaw(fractionDigits: coin.fractionDigits)); + _calculateFeesFuture = calculateFees( + 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), + ); _data = widget.autoFillData; walletId = widget.walletId; clipboard = widget.clipboard; @@ -962,10 +989,9 @@ class _SendViewState extends ConsumerState { fractionDigits: coin.fractionDigits, ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); } sendToController.text = _data.contactLabel; _address = _data.address.trim(); @@ -1051,7 +1077,8 @@ class _SendViewState extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final showCoinControl = wallet is CoinControlInterface && + final showCoinControl = + wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, @@ -1075,9 +1102,7 @@ class _SendViewState extends ConsumerState { }); } else { setState(() { - _calculateFeesFuture = calculateFees( - ref.read(pSendAmount)!, - ); + _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); }); } @@ -1135,11 +1160,7 @@ class _SendViewState extends ConsumerState { body: LayoutBuilder( builder: (builderContext, constraints) { return Padding( - padding: const EdgeInsets.only( - left: 12, - top: 12, - right: 12, - ), + padding: const EdgeInsets.only(left: 12, top: 12, right: 12), child: SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints( @@ -1154,9 +1175,10 @@ class _SendViewState extends ConsumerState { children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -1166,25 +1188,20 @@ class _SendViewState extends ConsumerState { child: Row( children: [ SvgPicture.file( - File( - ref.watch( - coinIconProvider(coin), - ), - ), + File(ref.watch(coinIconProvider(coin))), width: 22, height: 22, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( ref.watch(pWalletName(walletId)), - style: STextStyles.titleBold12(context) - .copyWith(fontSize: 14), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 14), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -1194,14 +1211,16 @@ class _SendViewState extends ConsumerState { if (isFiro) Text( "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), ), if (coin is! Firo) Text( "Available balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), ), ], ), @@ -1217,33 +1236,39 @@ class _SendViewState extends ConsumerState { ) .state) { case FiroType.public: - amount = ref - .read(pWalletBalance(walletId)) - .spendable; + amount = + ref + .read( + pWalletBalance(walletId), + ) + .spendable; break; case FiroType.lelantus: - amount = ref - .read( - pWalletBalanceSecondary( - walletId, - ), - ) - .spendable; + amount = + ref + .read( + pWalletBalanceSecondary( + walletId, + ), + ) + .spendable; break; case FiroType.spark: - amount = ref - .read( - pWalletBalanceTertiary( - walletId, - ), - ) - .spendable; + amount = + ref + .read( + pWalletBalanceTertiary( + walletId, + ), + ) + .spendable; break; } } else { - amount = ref - .read(pWalletBalance(walletId)) - .spendable; + amount = + ref + .read(pWalletBalance(walletId)) + .spendable; } return GestureDetector( @@ -1269,22 +1294,14 @@ class _SendViewState extends ConsumerState { .format(amount), style: STextStyles.titleBold12( context, - ).copyWith( - fontSize: 10, - ), + ).copyWith(fontSize: 10), textAlign: TextAlign.right, ), Text( - "${(amount.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${(amount.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, - ), + ).copyWith(fontSize: 8), textAlign: TextAlign.right, ), ], @@ -1297,9 +1314,7 @@ class _SendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1327,9 +1342,7 @@ class _SendViewState extends ConsumerState { // ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (isPaynymSend) TextField( key: const Key("sendViewPaynymAddressFieldKey"), @@ -1359,10 +1372,12 @@ class _SendViewState extends ConsumerState { paste: true, selectAll: false, ), - onChanged: (newValue) { + onChanged: (newValue) async { final trimmed = newValue.trim(); - if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) { + if ((trimmed.length - (_address?.length ?? 0)) + .abs() > + 1) { final parsed = AddressUtils.parsePaymentUri( trimmed, logging: Logging.instance, @@ -1370,11 +1385,15 @@ class _SendViewState extends ConsumerState { if (parsed != null) { _applyUri(parsed); } else { - _address = newValue; - sendToController.text = newValue; + await _checkSparkNameAndOrSetAddress( + newValue, + ); } } else { - _address = newValue; + await _checkSparkNameAndOrSetAddress( + newValue, + setController: false, + ); } _setValidAddressProviders(_address); @@ -1397,9 +1416,10 @@ class _SendViewState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: @@ -1407,87 +1427,37 @@ class _SendViewState extends ConsumerState { children: [ _addressToggleFlag ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Address Field Input.", - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _setValidAddressProviders( - _address, - ); - setState(() { - _addressToggleFlag = - false; - }); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _setValidAddressProviders( + _address, + ); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Address Field Input.", - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.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", - ), - ); - } - - if (coin is Epiccash) { - // strip http:// and https:// if content contains @ - content = AddressUtils() - .formatAddress( - content, - ); - } - - final trimmed = content.trim(); - final parsed = AddressUtils.parsePaymentUri( - trimmed, - logging: Logging.instance, - ); - if (parsed != null) { - _applyUri(parsed); - } else { - sendToController.text = - content; - _address = content; - - _setValidAddressProviders(_address,); - - setState(() { - _addressToggleFlag = - sendToController - .text - .isNotEmpty; - }); - } - } - }, - child: sendToController - .text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "sendViewPasteAddressFieldButtonKey", ), + onTap: _pasteAddress, + child: + sendToController + .text + .isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( semanticsLabel: @@ -1520,9 +1490,7 @@ class _SendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (isStellar || (ref.watch(pValidSparkSendToAddress) && ref.watch( @@ -1558,9 +1526,10 @@ class _SendViewState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: memoController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + memoController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: @@ -1568,42 +1537,41 @@ class _SendViewState extends ConsumerState { children: [ memoController.text.isNotEmpty ? TextFieldIconButton( - semanticsLabel: - "Clear Button. Clears The Memo Field Input.", - key: const Key( - "sendViewClearMemoFieldButtonKey", - ), - onTap: () { - memoController.text = ""; - setState(() {}); - }, - child: const XIcon(), - ) + semanticsLabel: + "Clear Button. Clears The Memo Field Input.", + key: const Key( + "sendViewClearMemoFieldButtonKey", + ), + onTap: () { + memoController.text = ""; + setState(() {}); + }, + child: const XIcon(), + ) : TextFieldIconButton( - semanticsLabel: - "Paste Button. Pastes From Clipboard To Memo Field Input.", - key: const Key( - "sendViewPasteMemoFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = - await clipboard.getData( - Clipboard.kTextPlain, - ); - if (data?.text != null && - data! - .text!.isNotEmpty) { - final String content = - data.text!.trim(); - - memoController.text = - content.trim(); - - setState(() {}); - } - }, - child: const ClipboardIcon(), + semanticsLabel: + "Paste Button. Pastes From Clipboard To Memo Field Input.", + key: const Key( + "sendViewPasteMemoFieldButtonKey", ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + final String content = + data.text!.trim(); + + memoController.text = + content.trim(); + + setState(() {}); + } + }, + child: const ClipboardIcon(), + ), ], ), ), @@ -1624,20 +1592,24 @@ class _SendViewState extends ConsumerState { FiroType.lelantus) { if (_data != null && _data.contactLabel == _address) { - error = SparkInterface.validateSparkAddress( - address: _data.address, - isTestNet: coin.network == - CryptoCurrencyNetwork.test, - ) - ? "Unsupported" - : null; - } else if (ref - .watch(pValidSparkSendToAddress)) { + error = + SparkInterface.validateSparkAddress( + address: _data.address, + isTestNet: + coin.network == + CryptoCurrencyNetwork.test, + ) + ? "Unsupported" + : null; + } else if (ref.watch( + pValidSparkSendToAddress, + )) { error = "Unsupported"; } else { - error = ref.watch(pValidSendToAddress) - ? null - : "Invalid address"; + error = + ref.watch(pValidSendToAddress) + ? null + : "Invalid address"; } } else { if (_data != null && @@ -1674,11 +1646,13 @@ class _SendViewState extends ConsumerState { child: Text( error, textAlign: TextAlign.left, - style: - STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + style: STextStyles.label( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textError, ), ), ), @@ -1686,20 +1660,14 @@ class _SendViewState extends ConsumerState { } }, ), - if (isFiro) - const SizedBox( - height: 12, - ), + if (isFiro) const SizedBox(height: 12), if (isFiro) Text( "Send from", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (isFiro) - const SizedBox( - height: 8, - ), + if (isFiro) const SizedBox(height: 8), if (isFiro) Stack( children: [ @@ -1715,9 +1683,10 @@ class _SendViewState extends ConsumerState { horizontal: 12, ), child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, + splashColor: + Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1732,10 +1701,10 @@ class _SendViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: (_) => - FiroBalanceSelectionSheet( - walletId: walletId, - ), + builder: + (_) => FiroBalanceSelectionSheet( + walletId: walletId, + ), ); }, child: Row( @@ -1750,9 +1719,7 @@ class _SendViewState extends ConsumerState { context, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Builder( builder: (_) { final Amount amount; @@ -1763,31 +1730,34 @@ class _SendViewState extends ConsumerState { ) .state) { case FiroType.public: - amount = ref - .watch( - pWalletBalance( - walletId, - ), - ) - .spendable; + amount = + ref + .watch( + pWalletBalance( + walletId, + ), + ) + .spendable; break; case FiroType.lelantus: - amount = ref - .watch( - pWalletBalanceSecondary( - walletId, - ), - ) - .spendable; + amount = + ref + .watch( + pWalletBalanceSecondary( + walletId, + ), + ) + .spendable; break; case FiroType.spark: - amount = ref - .watch( - pWalletBalanceTertiary( - walletId, - ), - ) - .spendable; + amount = + ref + .watch( + pWalletBalanceTertiary( + walletId, + ), + ) + .spendable; break; } @@ -1796,13 +1766,11 @@ class _SendViewState extends ConsumerState { .watch( pAmountFormatter(coin), ) - .format( - amount, - ), + .format(amount), style: STextStyles.itemSubtitle( - context, - ), + context, + ), ); }, ), @@ -1812,9 +1780,10 @@ class _SendViewState extends ConsumerState { Assets.svg.chevronDown, width: 8, height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, + color: + Theme.of(context) + .extension()! + .textSubtitle2, ), ], ), @@ -1822,9 +1791,7 @@ class _SendViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1843,27 +1810,28 @@ class _SendViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, + ), + key: const Key( + "amountInputFieldCryptoTextFieldKey", ), - key: - const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1888,10 +1856,9 @@ class _SendViewState extends ConsumerState { right: 12, ), hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + hintStyle: STextStyles.fieldLabel( + context, + ).copyWith(fontSize: 14), prefixIcon: FittedBox( fit: BoxFit.scaleDown, child: Padding( @@ -1900,11 +1867,13 @@ class _SendViewState extends ConsumerState { ref .watch(pAmountUnit(coin)) .unitForCoin(coin), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -1912,28 +1881,29 @@ class _SendViewState extends ConsumerState { ), ), if (Prefs.instance.externalCalls) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (Prefs.instance.externalCalls) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, + ), + key: const Key( + "amountInputFieldFiatTextFieldKey", ), - key: - const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1956,34 +1926,33 @@ class _SendViewState extends ConsumerState { right: 12, ), hintText: "0", - hintStyle: - STextStyles.fieldLabel(context).copyWith( - fontSize: 14, - ), + 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), + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), ), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), ), ), ), - if (showCoinControl) - const SizedBox( - height: 8, - ), + if (showCoinControl) const SizedBox(height: 8), if (showCoinControl) RoundedWhiteContainer( child: Row( @@ -1992,17 +1961,20 @@ class _SendViewState extends ConsumerState { children: [ Text( "Coin control", - style: - STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + style: STextStyles.w500_14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ), CustomTextButton( - text: selectedUTXOs.isEmpty - ? "Select coins" - : "Selected coins (${selectedUTXOs.length})", + text: + selectedUTXOs.isEmpty + ? "Select coins" + : "Selected coins (${selectedUTXOs.length})", onTap: () async { if (FocusScope.of(context).hasFocus) { FocusScope.of(context).unfocus(); @@ -2012,9 +1984,10 @@ class _SendViewState extends ConsumerState { } if (context.mounted) { - final spendable = ref - .read(pWalletBalance(walletId)) - .spendable; + final spendable = + ref + .read(pWalletBalance(walletId)) + .spendable; Amount? amount; if (ref.read(pSendAmount) != null) { @@ -2027,9 +2000,9 @@ class _SendViewState extends ConsumerState { } } - final result = - await Navigator.of(context) - .pushNamed( + final result = await Navigator.of( + context, + ).pushNamed( CoinControlView.routeName, arguments: Tuple4( walletId, @@ -2050,19 +2023,14 @@ class _SendViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (coin is Epiccash) Text( "On chain Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is Epiccash) - const SizedBox( - height: 8, - ), + if (coin is Epiccash) const SizedBox(height: 8), if (coin is Epiccash) ClipRRect( borderRadius: BorderRadius.circular( @@ -2082,35 +2050,33 @@ class _SendViewState extends ConsumerState { _onChainNoteFocusNode, context, ).copyWith( - suffixIcon: onChainNoteController - .text.isNotEmpty - ? Padding( - padding: - const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - onChainNoteController - .text = ""; - }); - }, - ), - ], + suffixIcon: + onChainNoteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + onChainNoteController + .text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), - if (coin is Epiccash) - const SizedBox( - height: 12, - ), + if (coin is Epiccash) const SizedBox(height: 12), Text( (coin is Epiccash) ? "Local Note (optional)" @@ -2118,9 +2084,7 @@ class _SendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -2137,32 +2101,32 @@ class _SendViewState extends ConsumerState { _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 = ""; - }); - }, - ), - ], + suffixIcon: + noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only( + right: 0, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos) @@ -2174,9 +2138,7 @@ class _SendViewState extends ConsumerState { if (coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos) @@ -2195,193 +2157,212 @@ class _SendViewState extends ConsumerState { horizontal: 12, ), child: RawMaterialButton( - splashColor: Theme.of(context) - .extension()! - .highlight, + splashColor: + Theme.of( + context, + ).extension()!.highlight, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), - onPressed: isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - FiroType.public - ? null - : () { - showModalBottomSheet( - backgroundColor: - Colors.transparent, - context: context, - shape: - const RoundedRectangleBorder( - borderRadius: - BorderRadius.vertical( - top: Radius.circular(20), - ), - ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - amount: (Decimal.tryParse( - cryptoAmountController - .text, - ) ?? - ref - .watch(pSendAmount) - ?.decimal ?? - Decimal.zero) - .toAmount( - fractionDigits: - coin.fractionDigits, - ), - updateChosen: (String fee) { - if (fee == "custom") { - if (!isCustomFee) { - setState(() { - isCustomFee = true; - }); - } - return; - } - - _setCurrentFee( - fee, - true, - ); - setState(() { - _calculateFeesFuture = - Future(() => fee); - if (isCustomFee) { - isCustomFee = false; - } - }); - }, - ), - ); - }, - child: (isFiro && - ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state != - FiroType.public) - ? Row( - children: [ - FutureBuilder( - future: _calculateFeesFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - _setCurrentFee( - snapshot.data!, - false, - ); - return Text( - "~${snapshot.data!}", - style: STextStyles - .itemSubtitle( - context, - ), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( - context, - ), - ); - } - }, - ), - ], - ) - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - ref + onPressed: + isFiro && + ref .watch( - feeRateTypeStateProvider + publicPrivateBalanceStateProvider .state, ) - .state - .prettyName, - style: STextStyles - .itemSubtitle12( - context, + .state != + FiroType.public + ? null + : () { + showModalBottomSheet( + backgroundColor: + Colors.transparent, + context: context, + shape: + const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical( + top: + Radius.circular( + 20, + ), + ), ), - ), - const SizedBox( - width: 10, - ), - FutureBuilder( - future: - _calculateFeesFuture, - builder: - (context, snapshot) { - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - _setCurrentFee( - snapshot.data!, - false, - ); - return Text( - isCustomFee - ? "" - : "~${snapshot.data!}", - style: STextStyles - .itemSubtitle( - context, + builder: + ( + _, + ) => TransactionFeeSelectionSheet( + walletId: walletId, + amount: (Decimal.tryParse( + cryptoAmountController + .text, + ) ?? + ref + .watch( + pSendAmount, + ) + ?.decimal ?? + Decimal.zero) + .toAmount( + fractionDigits: + coin.fractionDigits, ), + updateChosen: ( + String fee, + ) { + if (fee == "custom") { + if (!isCustomFee) { + setState(() { + isCustomFee = + true; + }); + } + return; + } + + _setCurrentFee( + fee, + true, ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ], - style: STextStyles - .itemSubtitle( + setState(() { + _calculateFeesFuture = + Future(() => fee); + if (isCustomFee) { + isCustomFee = false; + } + }); + }, + ), + ); + }, + child: + (isFiro && + ref + .watch( + publicPrivateBalanceStateProvider + .state, + ) + .state != + FiroType.public) + ? Row( + children: [ + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + _setCurrentFee( + snapshot.data!, + false, + ); + return Text( + "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeStateProvider + .state, + ) + .state + .prettyName, + style: + STextStyles.itemSubtitle12( context, ), - ); - } - }, - ), - ], - ), - SvgPicture.asset( - Assets.svg.chevronDown, - width: 8, - height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, - ), - ], - ), + ), + const SizedBox(width: 10), + FutureBuilder( + future: + _calculateFeesFuture, + builder: ( + context, + snapshot, + ) { + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + _setCurrentFee( + snapshot.data!, + false, + ); + return Text( + isCustomFee + ? "" + : "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textSubtitle2, + ), + ], + ), ), ), ], @@ -2400,28 +2381,26 @@ class _SendViewState extends ConsumerState { ), ), const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), TextButton( - onPressed: ref.watch(pPreviewTxButtonEnabled(coin)) - ? _previewTransaction - : null, - style: ref.watch(pPreviewTxButtonEnabled(coin)) - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + onPressed: + ref.watch(pPreviewTxButtonEnabled(coin)) + ? _previewTransaction + : null, + style: + ref.watch(pPreviewTxButtonEnabled(coin)) + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), child: Text( "Preview", style: STextStyles.button(context), ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), ], ), ), diff --git a/lib/pages/spark_names/buy_spark_name_view.dart b/lib/pages/spark_names/buy_spark_name_view.dart new file mode 100644 index 000000000..94c868e9d --- /dev/null +++ b/lib/pages/spark_names/buy_spark_name_view.dart @@ -0,0 +1,526 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; + +import '../../../providers/providers.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../db/drift/database.dart'; +import '../../models/isar/models/blockchain_data/address.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_buttons/blue_text_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; +import '../../widgets/rounded_white_container.dart'; +import 'confirm_spark_name_transaction_view.dart'; + +class BuySparkNameView extends ConsumerStatefulWidget { + const BuySparkNameView({ + super.key, + required this.walletId, + required this.name, + this.nameToRenew, + }); + + final String walletId; + final String name; + final SparkName? nameToRenew; + + static const routeName = "/buySparkNameView"; + + @override + ConsumerState createState() => _BuySparkNameViewState(); +} + +class _BuySparkNameViewState extends ConsumerState { + final addressController = TextEditingController(); + final additionalInfoController = TextEditingController(); + + bool get isRenewal => widget.nameToRenew != null; + String get _title => isRenewal ? "Renew name" : "Buy name"; + + int _years = 1; + + bool _lockAddressFill = false; + Future _fillCurrentReceiving() async { + if (_lockAddressFill) return; + _lockAddressFill = true; + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + final myAddress = await wallet.getCurrentReceivingSparkAddress(); + if (myAddress == null) { + throw Exception("No spark address found"); + } + addressController.text = myAddress.value; + } catch (e, s) { + Logging.instance.e("_fillCurrentReceiving", error: e, stackTrace: s); + } finally { + _lockAddressFill = false; + } + } + + Future _preRegFuture() async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + + final myAddresses = + await wallet.mainDB.isar.addresses + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .typeEqualTo(AddressType.spark) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .valueProperty() + .findAll(); + + final chosenAddress = addressController.text; + + if (!myAddresses.contains(chosenAddress)) { + throw Exception("Address does not belong to this wallet"); + } + + final txData = await wallet.prepareSparkNameTransaction( + name: widget.name, + address: chosenAddress, + years: _years, + additionalInfo: additionalInfoController.text, + ); + return txData; + } + + bool _preRegLock = false; + Future _prepareNameTx() async { + if (_preRegLock) return; + _preRegLock = true; + try { + final txData = + (await showLoading( + whileFuture: _preRegFuture(), + context: context, + message: "Preparing transaction...", + onException: (e) { + throw e; + }, + ))!; + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmSparkNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmSparkNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } catch (e, s) { + Logging.instance.e("_prepareNameTx failed", error: e, stackTrace: s); + + if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _preRegLock = false; + } + } + + @override + void initState() { + super.initState(); + if (isRenewal) { + additionalInfoController.text = widget.nameToRenew!.additionalInfo ?? ""; + addressController.text = widget.nameToRenew!.address; + } + } + + @override + void dispose() { + additionalInfoController.dispose(); + addressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + _title, + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ); + }, + child: Column( + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Name", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + Text( + widget.name, + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + CustomTextButton( + text: "Use current", + onTap: _fillCurrentReceiving, + ), + ], + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: addressController, + readOnly: isRenewal, + textAlignVertical: TextAlignVertical.center, + minLines: 1, + maxLines: 5, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintStyle: STextStyles.fieldLabel(context), + hintText: "Address", + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Additional info", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + ], + ), + const SizedBox(height: 4), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: additionalInfoController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + hintStyle: STextStyles.fieldLabel(context), + hintText: "Additional info", + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ), + ], + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${isRenewal ? "Renew" : "Register"} for", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + SizedBox( + width: Util.isDesktop ? 180 : 140, + child: DropdownButtonHideUnderline( + child: DropdownButton2( + value: _years, + items: [ + ...List.generate(10, (i) => i + 1).map( + (e) => DropdownMenuItem( + value: e, + child: Text( + "$e years", + style: STextStyles.w500_14(context), + ), + ), + ), + ], + onChanged: (value) { + if (value is int) { + setState(() { + _years = value; + }); + } + }, + isExpanded: true, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + maxHeight: 250, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: Util.isDesktop ? 16 : 8), + RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 0 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Cost", + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.infoItemLabel, + ), + ), + Text( + ref + .watch(pAmountFormatter(coin)) + .format( + Amount.fromDecimal( + Decimal.fromInt( + kStandardSparkNamesFee[widget.name.length] * _years, + ), + fractionDigits: coin.fractionDigits, + ), + ), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + + SizedBox(height: Util.isDesktop ? 32 : 16), + if (!Util.isDesktop) const Spacer(), + PrimaryButton( + label: isRenewal ? "Renew" : "Buy", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _prepareNameTx, + ), + SizedBox(height: Util.isDesktop ? 32 : 16), + ], + ), + ); + } +} diff --git a/lib/pages/spark_names/confirm_spark_name_transaction_view.dart b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart new file mode 100644 index 000000000..9afc5694f --- /dev/null +++ b/lib/pages/spark_names/confirm_spark_name_transaction_view.dart @@ -0,0 +1,975 @@ +/* + * 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:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../models/isar/models/transaction_note.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; +import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; +import '../../providers/providers.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../themes/theme_providers.dart'; +import '../../utilities/amount/amount.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/models/tx_data.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/rounded_container.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfield_icon_button.dart'; +import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/sending_transaction_dialog.dart'; + +class ConfirmSparkNameTransactionView extends ConsumerStatefulWidget { + const ConfirmSparkNameTransactionView({ + super.key, + required this.txData, + required this.walletId, + }); + + static const String routeName = "/confirmSparkNameTransactionView"; + + final TxData txData; + final String walletId; + + @override + ConsumerState createState() => + _ConfirmSparkNameTransactionViewState(); +} + +class _ConfirmSparkNameTransactionViewState + extends ConsumerState { + late final String walletId; + late final bool isDesktop; + + late final FocusNode _noteFocusNode; + late final TextEditingController noteController; + + Future _attemptSend() async { + final wallet = ref.read(pWallets).getWallet(walletId) as SparkInterface; + final coin = wallet.info.coin; + + final sendProgressController = ProgressAndSuccessController(); + + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return SendingTransactionDialog( + coin: coin, + controller: sendProgressController, + ); + }, + ), + ); + + final time = Future.delayed(const Duration(milliseconds: 2500)); + + final List txids = []; + Future txDataFuture; + + final note = noteController.text; + + try { + txDataFuture = wallet.confirmSendSpark(txData: widget.txData); + + // await futures in parallel + final futureResults = await Future.wait([txDataFuture, time]); + + final txData = (futureResults.first as TxData); + + sendProgressController.triggerSuccess?.call(); + + // await futures in parallel + await Future.wait([ + // wait for animation + Future.delayed(const Duration(seconds: 5)), + ]); + + txids.add(txData.txid!); + ref.refresh(desktopUseUTXOs); + + // save note + for (final txid in txids) { + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), + ); + } + + unawaited(wallet.refresh()); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop confirm send view + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop buy popup + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + } + } catch (e, s) { + const niceError = "Broadcast name transaction failed"; + + Logging.instance.e(niceError, error: e, stackTrace: s); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(niceError, style: STextStyles.desktopH3(context)), + const SizedBox(height: 24), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), + ), + ), + const SizedBox(height: 56), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return StackDialog( + title: niceError, + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } + }, + ); + } + } + } + + @override + void initState() { + isDesktop = Util.isDesktop; + walletId = widget.walletId; + _noteFocusNode = FocusNode(); + noteController = TextEditingController(); + noteController.text = widget.txData.note ?? ""; + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(walletId)); + + final unit = coin.ticker; + + final fee = widget.txData.fee; + final amountWithoutChange = widget.txData.amountWithoutChange!; + + return ConditionalParent( + condition: !isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: + () => + Navigator.of(context, rootNavigator: true).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + ], + ), + Flexible(child: SingleChildScrollView(child: child)), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Confirm Name transaction", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Name", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), + Text( + widget.txData.sparkNameInfo!.name, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Additional info", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + Text( + widget.txData.sparkNameInfo!.additionalInfo, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Recipient", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + Text( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Registration fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox(height: 12), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + const SizedBox(height: 12), + if (widget.txData.fee != null && widget.txData.vSize != null) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "sats/vByte", + style: STextStyles.smallMed12(context), + ), + const SizedBox(height: 4), + SelectableText( + "~${fee.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + const SizedBox(height: 12), + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), + SelectableText( + widget.txData.note!, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 50, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension()!.background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.background, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 22, + ), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.send, + ), + ), + ), + width: 32, + height: 32, + ), + const SizedBox(width: 16), + Text( + "Send $unit Name transaction", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Name", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.sparkNameInfo!.name, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), + Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Additional info", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.sparkNameInfo!.additionalInfo, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(left: 32, right: 32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "Note (optional)", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + autocorrect: isDesktop ? false : true, + enableSuggestions: isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + 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: 20), + ], + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Registration fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: Builder( + builder: (context) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + ); + String fiatAmount = "N/A"; + + if (externalCalls) { + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + .item1; + if (price > Decimal.zero) { + fiatAmount = (amountWithoutChange.decimal * price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: + ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, + ); + } + } + + return Row( + children: [ + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle(context), + ), + if (externalCalls) + Text( + " | ", + style: STextStyles.itemSubtitle(context), + ), + if (externalCalls) + SelectableText( + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle(context), + ), + ], + ); + }, + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Recipient", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle(context), + ), + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only(top: 16, left: 32), + child: Text( + "sats/vByte", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only(top: 10, left: 32, right: 32), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: SelectableText( + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox(height: isDesktop ? 23 : 12), + Padding( + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), + child: RoundedContainer( + padding: + isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: + Theme.of( + context, + ).extension()!.snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isDesktop ? "Total amount to send" : "Total amount", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange + fee!), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: + Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ), + SizedBox(height: isDesktop ? 28 : 16), + Padding( + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + final dynamic unlocked; + + if (isDesktop) { + unlocked = await showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: + (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: + "Confirm Transaction", + ), + settings: const RouteSettings( + name: "/confirmsendlockscreen", + ), + ), + ); + } + + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend()); + } else { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context, + ), + ); + } + } + } + }, + ), + ), + if (isDesktop) const SizedBox(height: 32), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/spark_names_home_view.dart b/lib/pages/spark_names/spark_names_home_view.dart new file mode 100644 index 000000000..e87889ce5 --- /dev/null +++ b/lib/pages/spark_names/spark_names_home_view.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/buy_spark_name_option_widget.dart'; +import 'sub_widgets/manage_spark_names_option_widget.dart'; + +class SparkNamesHomeView extends ConsumerStatefulWidget { + const SparkNamesHomeView({super.key, required this.walletId}); + + final String walletId; + + static const String routeName = "/sparkNamesHomeView"; + + @override + ConsumerState createState() => + _NamecoinNamesHomeViewState(); +} + +class _NamecoinNamesHomeViewState extends ConsumerState { + bool _onManage = true; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 20), + child: AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + ), + SvgPicture.asset( + Assets.svg.robotHead, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textDark, + ), + const SizedBox(width: 10), + Text("Names", style: STextStyles.desktopH3(context)), + ], + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Names", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: + (child) => SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: child, + ), + ), + child: + Util.isDesktop + ? Padding( + padding: const EdgeInsets.only(top: 24, left: 24, right: 24), + child: Row( + children: [ + SizedBox( + width: 460, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Register", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox(height: 14), + Flexible( + child: BuySparkNameOptionWidget( + walletId: widget.walletId, + ), + ), + ], + ), + ), + const SizedBox(width: 24), + Flexible( + child: SizedBox( + width: 520, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Names", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox(height: 14), + Flexible( + child: SingleChildScrollView( + child: ManageSparkNamesOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + onText: "Register", + offText: "Names", + isOn: !_onManage, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onManage = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox(height: 16), + Expanded( + child: IndexedStack( + index: _onManage ? 0 : 1, + children: [ + BuySparkNameOptionWidget(walletId: widget.walletId), + LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: SingleChildScrollView( + child: IntrinsicHeight( + child: ManageSparkNamesOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart new file mode 100644 index 000000000..18b6bb6f4 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/buy_spark_name_option_widget.dart @@ -0,0 +1,376 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../providers/providers.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../buy_spark_name_view.dart'; + +class BuySparkNameOptionWidget extends ConsumerStatefulWidget { + const BuySparkNameOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _BuySparkNameWidgetState(); +} + +class _BuySparkNameWidgetState extends ConsumerState { + final _nameController = TextEditingController(); + final _nameFieldFocus = FocusNode(); + + bool _isAvailable = false; + bool _isInvalidCharacters = false; + String? _lastLookedUpName; + + Future _checkIsAvailable(String name) async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + + try { + await wallet.electrumXClient.getSparkNameData(sparkName: name); + // name exists + return false; + } catch (e) { + if (e.toString().contains( + "(method not found): unknown method \"spark.getsparknamedata\"", + )) { + rethrow; + } + // name not found + return true; + } + } + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try { + _isAvailable = false; + + _lastLookedUpName = _nameController.text; + final result = await showLoading( + whileFuture: _checkIsAvailable(_lastLookedUpName!), + context: context, + message: "Searching...", + onException: (e) => throw e, + rootNavigator: Util.isDesktop, + delay: const Duration(seconds: 2), + ); + + _isAvailable = result == true; + + if (mounted) { + setState(() {}); + } + + Logging.instance.i("LOOKUP RESULT: $result"); + } catch (e, s) { + final String message; + if (e.toString().contains( + "(method not found): unknown method \"spark.getsparknamedata\"", + )) { + message = e.toString(); + } else { + message = "Spark name lookup failed"; + } + + Logging.instance.e(message, error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: message, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + width: 100, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.all( + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + inputFormatters: [ + LengthLimitingTextInputFormatter(kMaxNameLength), + ], + textInputAction: TextInputAction.search, + focusNode: _nameFieldFocus, + controller: _nameController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + prefixIcon: Padding( + padding: const EdgeInsets.all(14), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: + Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + fillColor: Colors.transparent, + hintText: "Find a spark name", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + onSubmitted: (_) { + if (_nameController.text.isNotEmpty) { + _lookup(); + } + }, + onChanged: (value) { + // trigger look up button enabled/disabled state change + setState(() { + _isInvalidCharacters = + value.isNotEmpty && + !RegExp(kNameRegexString).hasMatch(value); + }); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: + _isInvalidCharacters + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end, + children: [ + if (_isInvalidCharacters) + Text( + "Invalid name", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context).extension()!.textError, + ), + ), + Padding( + padding: const EdgeInsets.only(right: 5), + child: Builder( + builder: (context) { + final length = _nameController.text.length; + return Text( + "$length/$kMaxNameLength", + style: STextStyles.w500_10(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle2, + ), + ); + }, + ), + ), + ], + ), + SizedBox(height: Util.isDesktop ? 24 : 16), + SecondaryButton( + label: "Lookup", + enabled: + _nameController.text.isNotEmpty && + RegExp(kNameRegexString).hasMatch(_nameController.text), + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _lookup, + ), + const SizedBox(height: 32), + if (_lastLookedUpName != null) + _NameCard( + walletId: widget.walletId, + isAvailable: _isAvailable, + name: _lastLookedUpName!, + ), + ], + ); + } +} + +class _NameCard extends ConsumerWidget { + const _NameCard({ + super.key, + required this.walletId, + required this.isAvailable, + required this.name, + }); + + final String walletId; + final bool isAvailable; + final String name; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final availability = isAvailable ? "Available" : "Unavailable"; + final color = + isAvailable + ? Theme.of(context).extension()!.accentColorGreen + : Theme.of(context).extension()!.accentColorRed; + + final style = + (Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_12(context)); + + return RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: style), + const SizedBox(height: 4), + Text(availability, style: style.copyWith(color: color)), + ], + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PrimaryButton( + label: "Buy name", + enabled: isAvailable, + buttonHeight: + Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, + width: Util.isDesktop ? 140 : 120, + onPressed: () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy name", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuySparkNameView( + walletId: walletId, + name: name, + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuySparkNameView.routeName, + arguments: (walletId: walletId, name: name), + ); + } + } + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart b/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart new file mode 100644 index 000000000..18a554cee --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/manage_spark_names_option_widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../providers/db/drift_provider.dart'; +import '../../../utilities/util.dart'; +import 'owned_spark_name_card.dart'; + +class ManageSparkNamesOptionWidget extends ConsumerStatefulWidget { + const ManageSparkNamesOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => + _ManageSparkNamesWidgetState(); +} + +class _ManageSparkNamesWidgetState + extends ConsumerState { + @override + Widget build(BuildContext context) { + final db = ref.watch(pDrift(widget.walletId)); + return StreamBuilder( + stream: db.select(db.sparkNames).watch(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Column( + children: [ + ...snapshot.data!.map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 10), + child: OwnedSparkNameCard( + key: ValueKey(e), + name: e, + walletId: widget.walletId, + ), + ), + ), + SizedBox(height: Util.isDesktop ? 14 : 6), + ], + ); + } else { + return Container(); + } + }, + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart b/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart new file mode 100644 index 000000000..fc813efb9 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/owned_spark_name_card.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../db/drift/database.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import 'spark_name_details.dart'; + +class OwnedSparkNameCard extends ConsumerStatefulWidget { + const OwnedSparkNameCard({ + super.key, + required this.name, + required this.walletId, + }); + + final SparkName name; + final String walletId; + + @override + ConsumerState createState() => _OwnedSparkNameCardState(); +} + +class _OwnedSparkNameCardState extends ConsumerState { + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + final remaining = widget.name.validUntil - currentChainHeight; + + if (remaining <= 0) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (remaining < 1000) { + // todo change arbitrary 1000 to something else? + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + + return (message, color); + } + + bool _lock = false; + + Future _showDetails() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SparkNameDetailsView( + name: widget.name, + walletId: widget.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + SparkNameDetailsView.routeName, + arguments: (name: widget.name, walletId: widget.walletId), + ); + } + } finally { + _lock = false; + } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final (message, color) = _getExpiry( + ref.watch(pWalletChainHeight(widget.walletId)), + Theme.of(context).extension()!, + ); + + return RoundedWhiteContainer( + padding: + Util.isDesktop ? const EdgeInsets.all(16) : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(widget.name.name), + const SizedBox(height: 8), + SelectableText( + message, + style: STextStyles.w500_12(context).copyWith(color: color), + ), + ], + ), + const SizedBox(width: 12), + PrimaryButton( + label: "Details", + width: Util.isDesktop ? 90 : null, + buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _showDetails, + ), + ], + ), + ); + } +} diff --git a/lib/pages/spark_names/sub_widgets/spark_name_details.dart b/lib/pages/spark_names/sub_widgets/spark_name_details.dart new file mode 100644 index 000000000..3acaad8f5 --- /dev/null +++ b/lib/pages/spark_names/sub_widgets/spark_name_details.dart @@ -0,0 +1,464 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../db/drift/database.dart'; +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/db/drift_provider.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../widgets/background.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_container.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../buy_spark_name_view.dart'; + +class SparkNameDetailsView extends ConsumerStatefulWidget { + const SparkNameDetailsView({ + super.key, + required this.name, + required this.walletId, + }); + + static const routeName = "/sparkNameDetails"; + + final SparkName name; + final String walletId; + + @override + ConsumerState createState() => + _SparkNameDetailsViewState(); +} + +class _SparkNameDetailsViewState extends ConsumerState { + // todo change arbitrary 1000 to something else? + static const _remainingMagic = 1000; + + late Stream _nameStream; + late SparkName name; + + Stream? _labelStream; + AddressLabel? label; + + (String, Color, int) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + final remaining = name.validUntil - currentChainHeight; + + if (remaining <= 0) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (remaining < _remainingMagic) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + + return (message, color, remaining); + } + + bool _lock = false; + + Future _renew() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Renew name", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: BuySparkNameView( + walletId: widget.walletId, + name: name.name, + nameToRenew: name, + ), + ), + ], + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuySparkNameView.routeName, + arguments: ( + walletId: widget.walletId, + name: name.name, + nameToRenew: name, + ), + ); + } + } finally { + _lock = false; + } + } + + @override + void initState() { + super.initState(); + name = widget.name; + + label = ref + .read(mainDBProvider) + .getAddressLabelSync(widget.walletId, name.address); + + if (label != null) { + _labelStream = ref.read(mainDBProvider).watchAddressLabel(id: label!.id); + } + + final db = ref.read(pDrift(widget.walletId)); + + _nameStream = + (db.select(db.sparkNames) + ..where((e) => e.name.equals(name.name))).watchSingleOrNull(); + } + + @override + Widget build(BuildContext context) { + final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); + + final (message, color, remaining) = _getExpiry( + currentHeight, + Theme.of(context).extension()!, + ); + + return ConditionalParent( + condition: !Util.isDesktop, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + // Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Spark name details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight(child: child), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Spark name details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 10, + ), + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + child: child, + ), + ), + ], + ), + ); + }, + child: StreamBuilder( + stream: _nameStream, + builder: (context, snapshot) { + if (snapshot.hasData) { + name = snapshot.data!; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedContainer( + padding: const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SelectableText( + name.name, + style: + Util.isDesktop + ? STextStyles.pageTitleH2(context) + : STextStyles.w500_14(context), + ), + ], + ), + ), + + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton(data: name.address) + : SimpleCopyButton(data: name.address), + ], + ), + const SizedBox(height: 4), + SelectableText( + name.address, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (_labelStream != null) + StreamBuilder( + stream: _labelStream!, + builder: (context, snapshot) { + label = snapshot.data; + + return (label != null && label!.value.isNotEmpty) + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _Div(), + + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of( + context, + ).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address label", + style: STextStyles.w500_14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton(data: label!.value) + : SimpleCopyButton( + data: label!.value, + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + label!.value, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ], + ) + : const SizedBox(width: 0, height: 0); + }, + ), + + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Expiry", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + const SizedBox(height: 4), + SelectableText( + message, + style: STextStyles.w500_14( + context, + ).copyWith(color: color), + ), + ], + ), + if (remaining < _remainingMagic) + PrimaryButton( + label: "Renew", + buttonHeight: + Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _renew, + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: + Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + Util.isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Additional info", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, + ), + ), + const SizedBox(height: 4), + SelectableText( + name.additionalInfo ?? "", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + width: double.infinity, + height: 1.0, + color: Theme.of(context).extension()!.textFieldDefaultBG, + ); + } else { + return const SizedBox(height: 12); + } + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 41422c7e0..7356e07e6 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -98,6 +98,7 @@ import '../send_view/frost_ms/frost_send_view.dart'; import '../send_view/send_view.dart'; import '../settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import '../settings_views/wallet_settings_view/wallet_settings_view.dart'; +import '../spark_names/spark_names_home_view.dart'; import '../special/firo_rescan_recovery_error_dialog.dart'; import '../token_view/my_tokens_view.dart'; import 'sub_widgets/transactions_list.dart'; @@ -146,8 +147,9 @@ class _WalletViewState extends ConsumerState { bool _lelantusRescanRecovery = false; Future _firoRescanRecovery() async { - final success = await (ref.read(pWallets).getWallet(walletId) as FiroWallet) - .firoRescanRecovery(); + final success = + await (ref.read(pWallets).getWallet(walletId) as FiroWallet) + .firoRescanRecovery(); if (success) { // go into wallet @@ -160,10 +162,9 @@ class _WalletViewState extends ConsumerState { } else { // show error message dialog w/ options if (mounted) { - final shouldRetry = await Navigator.of(context).pushNamed( - FiroRescanRecoveryErrorView.routeName, - arguments: walletId, - ); + final shouldRetry = await Navigator.of( + context, + ).pushNamed(FiroRescanRecoveryErrorView.routeName, arguments: walletId); if (shouldRetry is bool && shouldRetry) { await _firoRescanRecovery(); @@ -218,41 +219,39 @@ class _WalletViewState extends ConsumerState { eventBus = widget.eventBus != null ? widget.eventBus! : GlobalEventBus.instance; - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case WalletSyncStatus.unableToSync: - // break; - // case WalletSyncStatus.synced: - // break; - // case WalletSyncStatus.syncing: - // break; - // } - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); - - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - // switch (event.newStatus) { - // case NodeConnectionStatus.disconnected: - // break; - // case NodeConnectionStatus.connected: - // break; - // } - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case WalletSyncStatus.unableToSync: + // break; + // case WalletSyncStatus.synced: + // break; + // case WalletSyncStatus.syncing: + // break; + // } + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); + + _nodeStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + // switch (event.newStatus) { + // case NodeConnectionStatus.disconnected: + // break; + // case NodeConnectionStatus.connected: + // break; + // } + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }); super.initState(); } @@ -379,9 +378,7 @@ class _WalletViewState extends ConsumerState { callerRouteName: WalletView.routeName, ); - await Navigator.of(context).pushNamed( - FrostStepScaffold.routeName, - ); + await Navigator.of(context).pushNamed(FrostStepScaffold.routeName); } Future _onExchangePressed(BuildContext context) async { @@ -390,24 +387,27 @@ class _WalletViewState extends ConsumerState { if (coin.network.isTestNet) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for test net coins", - ), + builder: + (_) => const StackOkDialog( + title: "Exchange not available for test net coins", + ), ); } else { Future _future; try { - _future = ExchangeDataLoadingService.instance.isar.currencies - .where() - .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst(); + _future = + ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst(); } catch (_) { _future = ExchangeDataLoadingService.instance.loadAll().then( - (_) => ExchangeDataLoadingService.instance.isar.currencies + (_) => + ExchangeDataLoadingService.instance.isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) .findFirst(), - ); + ); } final currency = await showLoading( @@ -436,9 +436,10 @@ class _WalletViewState extends ConsumerState { if (coin.network.isTestNet) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Buy not available for test net coins", - ), + builder: + (_) => const StackOkDialog( + title: "Buy not available for test net coins", + ), ); } else { if (mounted) { @@ -458,13 +459,14 @@ class _WalletViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - child: const CustomLoadingOverlay( - message: "Anonymizing balance", - eventBus: null, - ), - onWillPop: () async => shouldPop, - ), + builder: + (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Anonymizing balance", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), ), ); final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet; @@ -473,9 +475,9 @@ class _WalletViewState extends ConsumerState { if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); unawaited( showFloatingFlushBar( type: FlushBarType.info, @@ -492,9 +494,9 @@ class _WalletViewState extends ConsumerState { await firoWallet.anonymizeAllSpark(); shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); unawaited( showFloatingFlushBar( type: FlushBarType.success, @@ -506,15 +508,16 @@ class _WalletViewState extends ConsumerState { } catch (e) { shouldPop = true; if (mounted) { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); + Navigator.of( + context, + ).popUntil(ModalRoute.withName(WalletView.routeName)); await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Anonymize all failed", - message: "Reason: $e", - ), + builder: + (_) => StackOkDialog( + title: "Anonymize all failed", + message: "Reason: $e", + ), ); } } @@ -549,37 +552,46 @@ class _WalletViewState extends ConsumerState { eventBus: null, textColor: Theme.of(context).extension()!.textDark, - actionButton: _lelantusRescanRecovery - ? null - : SecondaryButton( - label: "Cancel", - onPressed: () async { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Warning!", - message: "Skipping this process can completely" - " break your wallet. It is only meant to be done in" - " emergency situations where the migration fails" - " and will not let you continue. Still skip?", - leftButton: SecondaryButton( - label: "Cancel", - onPressed: - Navigator.of(context, rootNavigator: true) - .pop, - ), - rightButton: SecondaryButton( - label: "Ok", - onPressed: () { - Navigator.of(context, rootNavigator: true) - .pop(); - setState(() => _rescanningOnOpen = false); - }, - ), - ), - ); - }, - ), + actionButton: + _lelantusRescanRecovery + ? null + : SecondaryButton( + label: "Cancel", + onPressed: () async { + await showDialog( + context: context, + builder: + (context) => StackDialog( + title: "Warning!", + message: + "Skipping this process can completely" + " break your wallet. It is only meant to be done in" + " emergency situations where the migration fails" + " and will not let you continue. Still skip?", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + rightButton: SecondaryButton( + label: "Ok", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + setState( + () => _rescanningOnOpen = false, + ); + }, + ), + ), + ); + }, + ), ), ), ], @@ -605,15 +617,11 @@ class _WalletViewState extends ConsumerState { title: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: Text( ref.watch(pWalletName(walletId)), @@ -625,15 +633,8 @@ class _WalletViewState extends ConsumerState { ), actions: [ const Padding( - padding: EdgeInsets.only( - top: 10, - bottom: 10, - right: 10, - ), - child: AspectRatio( - aspectRatio: 1, - child: SmallTorIcon(), - ), + padding: EdgeInsets.only(top: 10, bottom: 10, right: 10), + child: AspectRatio(aspectRatio: 1, child: SmallTorIcon()), ), Padding( padding: const EdgeInsets.only( @@ -649,9 +650,10 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewRadioButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: _buildNetworkIcon(_currentSyncStatus), onPressed: () { Navigator.of(context).pushNamed( @@ -680,91 +682,105 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewAlertsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, - icon: ref.watch( - notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor(walletId), - ), - ) - ? SvgPicture.file( - File( - ref.watch( - themeProvider.select( - (value) => value.assets.bellNew, - ), - ), - ), - width: 20, - height: 20, - color: ref.watch( + color: + Theme.of( + context, + ).extension()!.background, + icon: + ref.watch( notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId, - ), + (value) => value + .hasUnreadNotificationsFor(walletId), ), ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ) - : SvgPicture.asset( - Assets.svg.bell, - width: 20, - height: 20, - color: ref.watch( - notificationsProvider.select( - (value) => - value.hasUnreadNotificationsFor( - walletId, + ? SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.bellNew, + ), ), ), + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => value + .hasUnreadNotificationsFor( + walletId, + ), + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, ) - ? null - : Theme.of(context) - .extension()! - .topNavIconPrimary, - ), + : SvgPicture.asset( + Assets.svg.bell, + width: 20, + height: 20, + color: + ref.watch( + notificationsProvider.select( + (value) => value + .hasUnreadNotificationsFor( + walletId, + ), + ), + ) + ? null + : Theme.of(context) + .extension()! + .topNavIconPrimary, + ), onPressed: () { // reset unread state ref.refresh(unreadNotificationsStateProvider); Navigator.of(context) .pushNamed( - NotificationsView.routeName, - arguments: walletId, - ) + NotificationsView.routeName, + arguments: walletId, + ) .then((_) { - final Set unreadNotificationIds = ref - .read(unreadNotificationsStateProvider.state) - .state; - if (unreadNotificationIds.isEmpty) return; - - final List> futures = []; - for (int i = 0; - i < unreadNotificationIds.length - 1; - i++) { - futures.add( - ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.elementAt(i), - false, - ), - ); - } - - // wait for multiple to update if any - Future.wait(futures).then((_) { - // only notify listeners once - ref.read(notificationsProvider).markAsRead( - unreadNotificationIds.last, - true, + final Set unreadNotificationIds = + ref + .read( + unreadNotificationsStateProvider + .state, + ) + .state; + if (unreadNotificationIds.isEmpty) return; + + final List> futures = []; + for ( + int i = 0; + i < unreadNotificationIds.length - 1; + i++ + ) { + futures.add( + ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.elementAt(i), + false, + ), ); - }); - }); + } + + // wait for multiple to update if any + Future.wait(futures).then((_) { + // only notify listeners once + ref + .read(notificationsProvider) + .markAsRead( + unreadNotificationIds.last, + true, + ); + }); + }); }, ), ), @@ -783,14 +799,16 @@ class _WalletViewState extends ConsumerState { key: const Key("walletViewSettingsButton"), size: 36, shadows: const [], - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, icon: SvgPicture.asset( Assets.svg.bars, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -818,29 +836,25 @@ class _WalletViewState extends ConsumerState { Theme.of(context).extension()!.background, child: Column( children: [ - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: WalletSummary( walletId: walletId, aspectRatio: 1.75, - initialSyncStatus: ref - .watch(pWallets) - .getWallet(walletId) - .refreshMutex - .isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, + initialSyncStatus: + ref + .watch(pWallets) + .getWallet(walletId) + .refreshMutex + .isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, ), ), ), - if (isSparkWallet) - const SizedBox( - height: 10, - ), + if (isSparkWallet) const SizedBox(height: 10), if (isSparkWallet) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -856,51 +870,59 @@ class _WalletViewState extends ConsumerState { onPressed: () async { await showDialog( context: context, - builder: (context) => StackDialog( - title: "Attention!", - message: - "You're about to anonymize all of your public funds.", - leftButton: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + builder: + (context) => StackDialog( + title: "Attention!", + message: + "You're about to anonymize all of your public funds.", + leftButton: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + "Cancel", + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .accentColorDark, + ), + ), ), - ), - ), - rightButton: TextButton( - onPressed: () async { - Navigator.of(context).pop(); + rightButton: TextButton( + onPressed: () async { + Navigator.of(context).pop(); - unawaited(attemptAnonymize()); - }, - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle( - context, + unawaited(attemptAnonymize()); + }, + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle( + context, + ), + child: Text( + "Continue", + style: STextStyles.button( + context, + ), ), - child: Text( - "Continue", - style: - STextStyles.button(context), + ), ), - ), - ), ); }, child: Text( "Anonymize funds", - style: - STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), ), @@ -908,9 +930,7 @@ class _WalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( @@ -918,11 +938,13 @@ class _WalletViewState extends ConsumerState { children: [ Text( "Transactions", - style: - STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + style: STextStyles.itemSubtitle( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, ), ), CustomTextButton( @@ -943,9 +965,7 @@ class _WalletViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -970,11 +990,7 @@ class _WalletViewState extends ConsumerState { Colors.transparent, Colors.white, ], - stops: [ - 0.0, - 0.8, - 1.0, - ], + stops: [0.0, 0.8, 1.0], ).createShader(bounds); }, child: Container( @@ -989,17 +1005,20 @@ class _WalletViewState extends ConsumerState { CrossAxisAlignment.stretch, children: [ Expanded( - child: ref - .read(pWallets) - .getWallet(widget.walletId) - .isarTransactionVersion == - 2 - ? TransactionsV2List( - walletId: widget.walletId, - ) - : TransactionsList( - walletId: walletId, - ), + child: + ref + .read(pWallets) + .getWallet( + widget.walletId, + ) + .isarTransactionVersion == + 2 + ? TransactionsV2List( + walletId: widget.walletId, + ) + : TransactionsList( + walletId: walletId, + ), ), ], ), @@ -1059,10 +1078,7 @@ class _WalletViewState extends ConsumerState { wallet is BitcoinFrostWallet ? FrostSendView.routeName : SendView.routeName, - arguments: ( - walletId: walletId, - coin: coin, - ), + arguments: (walletId: walletId, coin: coin), ); }, ), @@ -1089,10 +1105,11 @@ class _WalletViewState extends ConsumerState { moreItems: [ if (ref.watch( pWallets.select( - (value) => value - .getWallet(widget.walletId) - .cryptoCurrency - .hasTokenSupport, + (value) => + value + .getWallet(widget.walletId) + .cryptoCurrency + .hasTokenSupport, ), )) WalletNavigationBarItemData( @@ -1111,9 +1128,10 @@ class _WalletViewState extends ConsumerState { Assets.svg.monkey, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .bottomNavIconIcon, + color: + Theme.of( + context, + ).extension()!.bottomNavIconIcon, ), label: "MonKey", onTap: () { @@ -1185,6 +1203,17 @@ class _WalletViewState extends ConsumerState { ); }, ), + if (wallet is SparkInterface) + WalletNavigationBarItemData( + label: "Names", + icon: const PaynymNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + SparkNamesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), if (!viewOnly && wallet is PaynymInterface) WalletNavigationBarItemData( label: "PayNym", @@ -1193,14 +1222,14 @@ class _WalletViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => const LoadingIndicator( - width: 100, - ), + builder: + (context) => const LoadingIndicator(width: 100), ), ); - final wallet = - ref.read(pWallets).getWallet(widget.walletId); + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); final paynymInterface = wallet as PaynymInterface; @@ -1219,10 +1248,10 @@ class _WalletViewState extends ConsumerState { // check if account exists and for matching code to see if claimed if (account.value != null && - account.value!.nonSegwitPaymentCode.claimed - // && - // account.value!.segwit - ) { + account.value!.nonSegwitPaymentCode.claimed + // && + // account.value!.segwit + ) { ref.read(myPaynymAccountStateProvider.state).state = account.value!; diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 2cf41f06a..e35e6ed34 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -423,73 +423,41 @@ class DesktopWalletHeaderRow extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return RoundedWhiteContainer( padding: const EdgeInsets.all(20), - child: - wallet is FiroWallet && MediaQuery.of(context).size.width < 1600 - ? Column( - children: [ - Row( - children: [ - SvgPicture.file( - File(ref.watch(coinIconProvider(wallet.info.coin))), - width: 40, - height: 40, - ), - const SizedBox(width: 10), - FiroDesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - - const Spacer(), - ], - ), - const SizedBox(height: 10), - Row( - children: [ - DesktopWalletFeatures(walletId: wallet.walletId), - ], - ), - ], - ) - : Row( - children: [ - if (monke != null) - SvgPicture.memory( - Uint8List.fromList(monke!), - width: 60, - height: 60, - ), - if (monke == null) - SvgPicture.file( - File(ref.watch(coinIconProvider(wallet.info.coin))), - width: 40, - height: 40, - ), - const SizedBox(width: 10), - if (wallet is FiroWallet) - FiroDesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), + child: Row( + children: [ + if (monke != null) + SvgPicture.memory( + Uint8List.fromList(monke!), + width: 60, + height: 60, + ), + if (monke == null) + SvgPicture.file( + File(ref.watch(coinIconProvider(wallet.info.coin))), + width: 40, + height: 40, + ), + const SizedBox(width: 10), + if (wallet is FiroWallet) + FiroDesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), - if (wallet is! FiroWallet) - DesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), - const Spacer(), - DesktopWalletFeatures(walletId: wallet.walletId), - ], - ), + if (wallet is! FiroWallet) + DesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + Expanded(child: DesktopWalletFeatures(walletId: wallet.walletId)), + ], + ), ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 009893e7e..fe65a90f0 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -29,6 +29,7 @@ import '../../../../providers/providers.dart'; import '../../../../providers/ui/fee_rate_type_state_provider.dart'; import '../../../../providers/ui/preview_tx_button_state_provider.dart'; import '../../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../../services/spark_names_service.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; import '../../../../utilities/amount/amount.dart'; @@ -155,13 +156,19 @@ class _DesktopSendState extends ConsumerState { try { _processQrCodeData(qrResult); } catch (e, s) { - Logging.instance - .e("Error processing QR code data", error: e, stackTrace: s); + Logging.instance.e( + "Error processing QR code data", + error: e, + stackTrace: s, + ); } } } catch (e, s) { - Logging.instance - .e("Error opening QR code scanner dialog", error: e, stackTrace: s); + Logging.instance.e( + "Error opening QR code scanner dialog", + error: e, + stackTrace: s, + ); } } @@ -204,10 +211,7 @@ class _DesktopSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -221,29 +225,20 @@ class _DesktopSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Text( "You are about to send your entire balance. Would you like to continue?", textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Row( children: [ Expanded( @@ -255,9 +250,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -301,7 +294,8 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(32), child: BuildingTransactionDialog( coin: wallet.info.coin, - isSpark: wallet is FiroWallet && + isSpark: + wallet is FiroWallet && ref .read(publicPrivateBalanceStateProvider.state) .state == @@ -319,11 +313,7 @@ class _DesktopSendState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; @@ -344,11 +334,12 @@ class _DesktopSendState extends ConsumerState { ], satsPerVByte: isCustomFee ? customFeeRate : null, feeRateType: feeRate, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, ), ); } else if (wallet is FiroWallet) { @@ -367,30 +358,28 @@ class _DesktopSendState extends ConsumerState { ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, ), ); } else { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), + (address: _address!, amount: amount, isChange: false), ], feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, ), ); } @@ -400,11 +389,7 @@ class _DesktopSendState extends ConsumerState { txDataFuture = wallet.prepareSendLelantus( txData: TxData( recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), + (address: _address!, amount: amount, isChange: false), ], ), ); @@ -413,25 +398,23 @@ class _DesktopSendState extends ConsumerState { case FiroType.spark: txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: ref.read(pValidSparkSendToAddress) - ? null - : [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - sparkRecipients: ref.read(pValidSparkSendToAddress) - ? [ - ( - address: _address!, - amount: amount, - memo: memoController.text, - isChange: false, - ), - ] - : null, + recipients: + ref.read(pValidSparkSendToAddress) + ? null + : [ + (address: _address!, amount: amount, isChange: false), + ], + sparkRecipients: + ref.read(pValidSparkSendToAddress) + ? [ + ( + address: _address!, + amount: amount, + memo: memoController.text, + isChange: false, + ), + ] + : null, ), ); break; @@ -440,29 +423,21 @@ class _DesktopSendState extends ConsumerState { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], + recipients: [(address: _address!, amount: amount, isChange: false)], memo: memo, feeRateType: ref.read(feeRateTypeStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -473,35 +448,29 @@ class _DesktopSendState extends ConsumerState { note: _note ?? "PayNym send", ); } else { - txData = txData.copyWith( - note: _note ?? "", - ); + txData = txData.copyWith(note: _note ?? ""); if (coin is Epiccash) { - txData = txData.copyWith( - noteOnChain: _onChainNote ?? "", - ); + txData = txData.copyWith(noteOnChain: _onChainNote ?? ""); } } // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - txData: txData, - walletId: walletId, - onSuccess: clearSendForm, - isPaynymTransaction: isPaynymSend, - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData, + walletId: walletId, + onSuccess: clearSendForm, + isPaynymTransaction: isPaynymSend, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } @@ -509,10 +478,7 @@ class _DesktopSendState extends ConsumerState { Logging.instance.e("Desktop send: ", error: e, stackTrace: s); if (mounted) { // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( @@ -522,10 +488,7 @@ class _DesktopSendState extends ConsumerState { maxWidth: 450, maxHeight: double.infinity, child: Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, bottom: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -539,25 +502,18 @@ class _DesktopSendState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.only( - right: 32, - ), + padding: const EdgeInsets.only(right: 32), child: Text( e.toString(), textAlign: TextAlign.left, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - fontSize: 18, - ), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith(fontSize: 18), ), ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), Row( children: [ Expanded( @@ -572,9 +528,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), ], ), ], @@ -602,9 +556,9 @@ class _DesktopSendState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text); final Amount? amount; if (cryptoAmount != null) { amount = cryptoAmount; @@ -685,14 +639,15 @@ class _DesktopSendState extends ConsumerState { } else { final wallet = ref.read(pWallets).getWallet(walletId); if (wallet is SparkInterface) { - ref.read(pValidSparkSendToAddress.notifier).state = - SparkInterface.validateSparkAddress( + ref + .read(pValidSparkSendToAddress.notifier) + .state = SparkInterface.validateSparkAddress( address: address ?? "", isTestNet: wallet.cryptoCurrency.network.isTestNet, ); - ref.read(pIsExchangeAddress.state).state = - (coin as Firo).isExchangeAddress(address ?? ""); + ref.read(pIsExchangeAddress.state).state = (coin as Firo) + .isExchangeAddress(address ?? ""); if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && ref.read(pIsExchangeAddress) && @@ -705,8 +660,8 @@ class _DesktopSendState extends ConsumerState { } } - ref.read(pValidSendToAddress.notifier).state = - wallet.cryptoCurrency.validateAddress(address ?? ""); + ref.read(pValidSendToAddress.notifier).state = wallet.cryptoCurrency + .validateAddress(address ?? ""); } } @@ -725,9 +680,9 @@ class _DesktopSendState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: coin.fractionDigits, - ); + final amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: coin.fractionDigits); cryptoAmountController.text = ref .read(pAmountFormatter(coin)) .format(amount, withUnitName: false); @@ -744,12 +699,41 @@ class _DesktopSendState extends ConsumerState { } } + Future _checkSparkNameAndOrSetAddress( + String content, { + bool setController = true, + }) async { + void setContent() { + if (setController) { + sendToController.text = content; + } + _address = content; + } + + // check for spark name + if (coin is Firo) { + final address = await SparkNamesService.getAddressFor( + content, + network: coin.network, + ); + if (address != null) { + // found a spark name + sendToController.text = content; + _address = address; + } else { + setContent(); + } + } else { + setContent(); + } + } + Future pasteAddress() async { final ClipboardData? data = await clipboard.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")); + content = content.substring(0, content.indexOf("\n")).trim(); } try { @@ -761,7 +745,6 @@ class _DesktopSendState extends ConsumerState { paymentData.coin?.uriScheme == coin.uriScheme) { _applyUri(paymentData); } else { - content = content.split("\n").first.trim(); if (coin is Epiccash) { content = AddressUtils().formatAddress(content); } @@ -781,8 +764,7 @@ class _DesktopSendState extends ConsumerState { content = AddressUtils().formatAddress(content); } - sendToController.text = content; - _address = content; + await _checkSparkNameAndOrSetAddress(content); // Trigger validation after pasting. _setValidAddressProviders(_address); @@ -823,21 +805,21 @@ class _DesktopSendState extends ConsumerState { if (_price == Decimal.zero) { amount = Decimal.zero.toAmount(fractionDigits: coin.fractionDigits); } else { - amount = baseAmount <= Amount.zero - ? Decimal.zero.toAmount(fractionDigits: coin.fractionDigits) - : (baseAmount.decimal / _price) - .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) - .toAmount(fractionDigits: coin.fractionDigits); + amount = + baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: coin.fractionDigits) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.fractionDigits) + .toAmount(fractionDigits: coin.fractionDigits); } if (_cachedAmountToSend != null && _cachedAmountToSend == amount) { return; } _cachedAmountToSend = amount; - final amountString = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + final amountString = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; @@ -865,10 +847,9 @@ class _DesktopSendState extends ConsumerState { } Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: - utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), - fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, - ); + rawValue: utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), + fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + ); Future _sendAllTapped(bool showCoinControl) async { final Amount amount; @@ -891,20 +872,20 @@ class _DesktopSendState extends ConsumerState { amount = ref.read(pWalletBalance(walletId)).spendable; } - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); } void _showDesktopCoinControl() async { final amount = ref.read(pSendAmount); await showDialog( context: context, - builder: (context) => DesktopCoinControlUseDialog( - walletId: widget.walletId, - amountToSend: amount, - ), + builder: + (context) => DesktopCoinControlUseDialog( + walletId: widget.walletId, + amountToSend: amount, + ), ); } @@ -1033,7 +1014,8 @@ class _DesktopSendState extends ConsumerState { } }); - final showCoinControl = ref.watch( + final showCoinControl = + ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), @@ -1044,23 +1026,19 @@ class _DesktopSendState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), if (coin is Firo) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - if (coin is Firo) - const SizedBox( - height: 10, - ), + if (coin is Firo) const SizedBox(height: 10), if (coin is Firo) DropdownButtonHideUnderline( child: DropdownButton2( @@ -1075,11 +1053,11 @@ class _DesktopSendState extends ConsumerState { "Spark balance", style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref .watch(pWalletBalanceTertiary(walletId)) .spendable, @@ -1099,11 +1077,11 @@ class _DesktopSendState extends ConsumerState { "Lelantus balance", style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref .watch(pWalletBalanceSecondary(walletId)) .spendable, @@ -1121,11 +1099,11 @@ class _DesktopSendState extends ConsumerState { "Public balance", style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref.watch(pWalletBalance(walletId)).spendable, ), style: STextStyles.itemSubtitle(context), @@ -1157,45 +1135,37 @@ class _DesktopSendState extends ConsumerState { offset: const Offset(0, -10), elevation: 0, decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), ), menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), - if (coin is Firo) - const SizedBox( - height: 20, - ), + if (coin is Firo) const SizedBox(height: 20), if (isPaynymSend) Text( "Send to PayNym address", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (isPaynymSend) - const SizedBox( - height: 10, - ), + if (isPaynymSend) const SizedBox(height: 10), if (isPaynymSend) TextField( key: const Key("sendViewPaynymAddressFieldKey"), controller: sendToController, enabled: false, readOnly: true, - style: STextStyles.desktopTextFieldLabel(context).copyWith( - fontSize: 16, - ), + style: STextStyles.desktopTextFieldLabel( + context, + ).copyWith(fontSize: 16), decoration: const InputDecoration( contentPadding: EdgeInsets.symmetric( vertical: 18, @@ -1203,19 +1173,17 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (isPaynymSend) - const SizedBox( - height: 20, - ), + if (isPaynymSend) const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -1229,9 +1197,7 @@ class _DesktopSendState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -1241,12 +1207,13 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldCryptoTextFieldKey"), controller: cryptoAmountController, focusNode: _cryptoFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ AmountInputFormatter( @@ -1270,9 +1237,10 @@ class _DesktopSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1281,19 +1249,17 @@ class _DesktopSendState extends ConsumerState { child: Text( ref.watch(pAmountUnit(coin)).unitForCoin(coin), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), ), ), - if (Prefs.instance.externalCalls) - const SizedBox( - height: 10, - ), + if (Prefs.instance.externalCalls) const SizedBox(height: 10), if (Prefs.instance.externalCalls) TextField( autocorrect: Util.isDesktop ? false : true, @@ -1304,18 +1270,16 @@ class _DesktopSendState extends ConsumerState { key: const Key("amountInputFieldFiatTextFieldKey"), controller: baseAmountController, focusNode: _baseFocus, - keyboardType: Util.isDesktop - ? null - : const TextInputType.numberWithOptions( - signed: false, - decimal: true, - ), + keyboardType: + Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), textAlign: TextAlign.right, inputFormatters: [ - AmountInputFormatter( - decimals: 2, - locale: locale, - ), + AmountInputFormatter(decimals: 2, locale: locale), // // regex to validate a fiat amount with 2 decimal places // TextInputFormatter.withFunction((oldValue, newValue) => // RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') @@ -1332,9 +1296,10 @@ class _DesktopSendState extends ConsumerState { ), hintText: "0", hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldDefaultText, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultText, ), prefixIcon: FittedBox( fit: BoxFit.scaleDown, @@ -1342,23 +1307,22 @@ class _DesktopSendState extends ConsumerState { padding: const EdgeInsets.all(12), child: Text( ref.watch( - prefsChangeNotifierProvider - .select((value) => value.currency), + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), ), style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), ), ), ), - if (showCoinControl) - const SizedBox( - height: 10, - ), + if (showCoinControl) const SizedBox(height: 10), if (showCoinControl) RoundedContainer( color: Colors.transparent, @@ -1372,31 +1336,28 @@ class _DesktopSendState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall(context), ), CustomTextButton( - text: ref.watch(desktopUseUTXOs.state).state.isEmpty - ? "Select coins" - : "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})", + text: + ref.watch(desktopUseUTXOs.state).state.isEmpty + ? "Select coins" + : "Selected coins (${ref.watch(desktopUseUTXOs.state).state.length})", onTap: _showDesktopCoinControl, ), ], ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), if (!isPaynymSend) Text( "Send to", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - if (!isPaynymSend) - const SizedBox( - height: 10, - ), + if (!isPaynymSend) const SizedBox(height: 10), if (!isPaynymSend) ClipRRect( borderRadius: BorderRadius.circular( @@ -1420,19 +1381,24 @@ class _DesktopSendState extends ConsumerState { paste: true, selectAll: false, ), - onChanged: (newValue) { + onChanged: (newValue) async { final trimmed = newValue; if ((trimmed.length - (_address?.length ?? 0)).abs() > 1) { - final parsed = AddressUtils.parsePaymentUri(trimmed, logging: Logging.instance); + final parsed = AddressUtils.parsePaymentUri( + trimmed, + logging: Logging.instance, + ); if (parsed != null) { _applyUri(parsed); } else { - _address = newValue; - sendToController.text = newValue; + await _checkSparkNameAndOrSetAddress(newValue); } } else { - _address = newValue; + await _checkSparkNameAndOrSetAddress( + newValue, + setController: false, + ); } _setValidAddressProviders(_address); @@ -1443,9 +1409,10 @@ class _DesktopSendState extends ConsumerState { }, focusNode: _addressFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1461,76 +1428,80 @@ class _DesktopSendState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: sendToController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _setValidAddressProviders(_address); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _setValidAddressProviders(_address); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: pasteAddress, - child: sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "sendViewPasteAddressFieldButtonKey", ), + onTap: pasteAddress, + child: + sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendViewAddressBookButtonKey"), onTap: () async { - final entry = - await showDialog( + final entry = await showDialog< + ContactAddressEntry? + >( context: context, - builder: (context) => DesktopDialog( - maxWidth: 696, - maxHeight: 600, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Address book", - style: STextStyles.desktopH3( - context, + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3( + context, + ), + ), ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { @@ -1552,9 +1523,7 @@ class _DesktopSendState extends ConsumerState { TextFieldIconButton( semanticsLabel: "Scan QR Button. Opens Camera For Scanning QR Code.", - key: const Key( - "sendViewScanQrButtonKey", - ), + key: const Key("sendViewScanQrButtonKey"), onTap: scanWebcam, child: const QrCodeIcon(), ), @@ -1575,18 +1544,20 @@ class _DesktopSendState extends ConsumerState { } else if (coin is Firo) { if (firoType == FiroType.lelantus) { if (_data != null && _data.contactLabel == _address) { - error = SparkInterface.validateSparkAddress( - address: _data.address, - isTestNet: coin.network.isTestNet, - ) - ? "Lelantus to Spark not supported" - : null; + error = + SparkInterface.validateSparkAddress( + address: _data.address, + isTestNet: coin.network.isTestNet, + ) + ? "Lelantus to Spark not supported" + : null; } else if (ref.watch(pValidSparkSendToAddress)) { error = "Lelantus to Spark not supported"; } else { - error = ref.watch(pValidSendToAddress) - ? null - : "Invalid address"; + error = + ref.watch(pValidSendToAddress) + ? null + : "Invalid address"; } } else { if (_data != null && _data.contactLabel == _address) { @@ -1614,17 +1585,15 @@ class _DesktopSendState extends ConsumerState { return Align( alignment: Alignment.topLeft, child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - top: 4.0, - ), + padding: const EdgeInsets.only(left: 12.0, top: 4.0), child: Text( error, textAlign: TextAlign.left, style: STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .textError, + color: + Theme.of( + context, + ).extension()!.textError, ), ), ), @@ -1635,9 +1604,7 @@ class _DesktopSendState extends ConsumerState { if (isStellar || (ref.watch(pValidSparkSendToAddress) && firoType != FiroType.lelantus)) - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (isStellar || (ref.watch(pValidSparkSendToAddress) && firoType != FiroType.lelantus)) @@ -1659,9 +1626,10 @@ class _DesktopSendState extends ConsumerState { setState(() {}); }, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1678,9 +1646,10 @@ class _DesktopSendState extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: memoController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + memoController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -1688,9 +1657,10 @@ class _DesktopSendState extends ConsumerState { TextFieldIconButton( key: const Key("sendViewPasteMemoButtonKey"), onTap: pasteMemo, - child: memoController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + child: + memoController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), ), ], ), @@ -1699,14 +1669,11 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (!isPaynymSend) - const SizedBox( - height: 20, - ), + if (!isPaynymSend) const SizedBox(height: 20), if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) ConditionalParent( - condition: ref.watch(pWallets).getWallet(walletId) - is ElectrumXInterface && + condition: + ref.watch(pWallets).getWallet(walletId) is ElectrumXInterface && !(((coin is Firo) && (ref.watch(publicPrivateBalanceStateProvider.state).state == FiroType.lelantus || @@ -1714,165 +1681,163 @@ class _DesktopSendState extends ConsumerState { .watch(publicPrivateBalanceStateProvider.state) .state == FiroType.spark))), - builder: (child) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - child, - CustomTextButton( - text: "Edit", - onTap: () async { - feeSelectionResult = await showDialog< - ( - FeeRateType, - String?, - String?, - )?>( - context: context, - builder: (_) => DesktopFeeDialog( - walletId: walletId, - ), - ); - - if (feeSelectionResult != null) { - if (isCustomFee && - feeSelectionResult!.$1 != FeeRateType.custom) { - isCustomFee = false; - } else if (!isCustomFee && - feeSelectionResult!.$1 == FeeRateType.custom) { - isCustomFee = true; - } - } - - setState(() {}); - }, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + CustomTextButton( + text: "Edit", + onTap: () async { + feeSelectionResult = + await showDialog<(FeeRateType, String?, String?)?>( + context: context, + builder: + (_) => DesktopFeeDialog(walletId: walletId), + ); + + if (feeSelectionResult != null) { + if (isCustomFee && + feeSelectionResult!.$1 != FeeRateType.custom) { + isCustomFee = false; + } else if (!isCustomFee && + feeSelectionResult!.$1 == FeeRateType.custom) { + isCustomFee = true; + } + } + + setState(() {}); + }, + ), + ], ), - ], - ), child: Text( "Transaction fee" "${isCustomFee ? "" : " (${coin is Ethereum ? "max" : "estimated"})"}", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), ), if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) if (!isCustomFee) Padding( padding: const EdgeInsets.all(10), - child: (feeSelectionResult?.$2 == null) - ? FutureBuilder( - future: ref.watch( - pWallets.select( - (value) => value.getWallet(walletId).fees, + child: + (feeSelectionResult?.$2 == null) + ? FutureBuilder( + future: ref.watch( + pWallets.select( + (value) => value.getWallet(walletId).fees, + ), ), - ), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return DesktopFeeItem( - feeObject: snapshot.data, - feeRateType: FeeRateType.average, - walletId: walletId, - isButton: false, - feeFor: ({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) async { - if (ref - .read(feeSheetSessionCacheProvider) - .average[amount] == - null) { - final wallet = - ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.medium.value, - ); - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = fee; - } else if ((coin is Firo) && + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return DesktopFeeItem( + feeObject: snapshot.data, + feeRateType: FeeRateType.average, + walletId: walletId, + isButton: false, + feeFor: ({ + required Amount amount, + required FeeRateType feeRateType, + required int feeRate, + required CryptoCurrency coin, + }) async { + if (ref + .read(feeSheetSessionCacheProvider) + .average[amount] == + null) { + final wallet = ref + .read(pWallets) + .getWallet(walletId); + + if (coin is Monero || coin is Wownero) { + final fee = await wallet.estimateFeeFor( + amount, + lib_monero + .TransactionPriority + .medium + .value, + ); ref + .read(feeSheetSessionCacheProvider) + .average[amount] = + fee; + } else if ((coin is Firo) && + ref + .read( + publicPrivateBalanceStateProvider + .state, + ) + .state != + FiroType.public) { + final firoWallet = wallet as FiroWallet; + + if (ref .read( publicPrivateBalanceStateProvider .state, ) - .state != - FiroType.public) { - final firoWallet = wallet as FiroWallet; - - if (ref - .read( - publicPrivateBalanceStateProvider - .state, - ) - .state == - FiroType.lelantus) { - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await firoWallet - .estimateFeeForLelantus(amount); - } else if (ref - .read( - publicPrivateBalanceStateProvider - .state, - ) - .state == - FiroType.spark) { + .state == + FiroType.lelantus) { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = await firoWallet + .estimateFeeForLelantus(amount); + } else if (ref + .read( + publicPrivateBalanceStateProvider + .state, + ) + .state == + FiroType.spark) { + ref + .read(feeSheetSessionCacheProvider) + .average[amount] = await firoWallet + .estimateFeeForSpark(amount); + } + } else { ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await firoWallet - .estimateFeeForSpark(amount); + .read(feeSheetSessionCacheProvider) + .average[amount] = await wallet + .estimateFeeFor(amount, feeRate); } - } else { - ref - .read(feeSheetSessionCacheProvider) - .average[amount] = - await wallet.estimateFeeFor( - amount, - feeRate, - ); } - } - return ref - .read(feeSheetSessionCacheProvider) - .average[amount]!; - }, - isSelected: true, - ); - } else { - return Row( - children: [ - AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + return ref + .read(feeSheetSessionCacheProvider) + .average[amount]!; + }, + isSelected: true, + ); + } else { + return Row( + children: [ + AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + ), ), - ), - ], - ); - } - }, - ) - : (coin is Firo) && + ], + ); + } + }, + ) + : (coin is Firo) && ref .watch( publicPrivateBalanceStateProvider.state, @@ -1880,54 +1845,49 @@ class _DesktopSendState extends ConsumerState { .state == FiroType.lelantus ? Text( - "~${ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.parse("3794"), - fractionDigits: coin.fractionDigits, - ), - indicatePrecisionLoss: false, - )}", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - feeSelectionResult?.$2 ?? "", - style: STextStyles.desktopTextExtraExtraSmall( + "~${ref.watch(pAmountFormatter(coin)).format(Amount(rawValue: BigInt.parse("3794"), fractionDigits: coin.fractionDigits), indicatePrecisionLoss: false)}", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, + ).extension()!.textFieldActiveText, + ), + textAlign: TextAlign.left, + ) + : Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + feeSelectionResult?.$2 ?? "", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, ), - Text( - feeSelectionResult?.$3 ?? "", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), + textAlign: TextAlign.left, + ), + Text( + feeSelectionResult?.$3 ?? "", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), - ], - ), + ), + ], + ), ), if (isCustomFee) Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, - ), + padding: const EdgeInsets.only(bottom: 12, top: 16), child: FeeSlider( coin: coin, onSatVByteChanged: (rate) { @@ -1935,9 +1895,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 78a36d8d7..814af0cd2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -9,6 +9,7 @@ */ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,16 +17,19 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; import '../../../../app_config.dart'; +import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/monkey/monkey_view.dart'; import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; +import '../../../../pages/spark_names/spark_names_home_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; import '../../../../providers/global/paynym_api_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../providers/wallet/my_paynym_account_state_provider.dart'; import '../../../../themes/stack_colors.dart'; +import '../../../../themes/theme_providers.dart'; import '../../../../utilities/amount/amount.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; @@ -34,16 +38,23 @@ import '../../../../utilities/text_styles.dart'; import '../../../../wallets/crypto_currency/coins/banano.dart'; import '../../../../wallets/crypto_currency/coins/firo.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; +import '../../../../wallets/wallet/wallet.dart' show Wallet; import '../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/custom_loading_overlay.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/loading_indicator.dart'; +import '../../../../widgets/static_overflow_row/static_overflow_row.dart'; import '../../../cashfusion/desktop_cashfusion_view.dart'; import '../../../churning/desktop_churning_view.dart'; import '../../../coin_control/desktop_coin_control_view.dart'; @@ -54,6 +65,35 @@ import '../../../spark_coins/spark_coins_view.dart'; import '../desktop_wallet_view.dart'; import 'more_features/more_features_dialog.dart'; +enum WalletFeature { + anonymizeFunds("Anonymize funds", "Anonymize funds"), + swap("Swap", ""), + buy("Buy", "Buy cryptocurrency"), + paynym("PayNym", "Increased address privacy using BIP47"), + coinControl( + "Coin control", + "Control, freeze, and utilize outputs at your discretion", + ), + lelantusCoins("Lelantus coins", "View wallet lelantus coins"), + sparkCoins("Spark coins", "View wallet spark coins"), + ordinals("Ordinals", "View and control your ordinals in ${AppConfig.prefix}"), + monkey("MonKey", "Generate Banano MonKey"), + fusion("Fusion", "Decentralized mixing protocol"), + churn("Churn", "Churning"), + namecoinName("Domains", "Namecoin DNS"), + sparkNames("Names", "Spark names"), + + // special cases + clearSparkCache("", ""), + lelantusScanOption("", ""), + rbf("", ""), + reuseAddress("", ""); + + final String label; + final String description; + const WalletFeature(this.label, this.description); +} + class DesktopWalletFeatures extends ConsumerStatefulWidget { const DesktopWalletFeatures({super.key, required this.walletId}); @@ -65,8 +105,6 @@ class DesktopWalletFeatures extends ConsumerStatefulWidget { } class _DesktopWalletFeaturesState extends ConsumerState { - static const double buttonWidth = 120; - Future _onSwapPressed() async { ref.read(currentDesktopMenuItemProvider.state).state = DesktopMenuItemId.exchange; @@ -75,57 +113,35 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onBuyPressed() async { - Navigator.of(context, rootNavigator: true).pop(); ref.read(currentDesktopMenuItemProvider.state).state = DesktopMenuItemId.buy; ref.read(prevDesktopMenuItemProvider.state).state = DesktopMenuItemId.buy; } - Future _onMorePressed() async { + Future _onMorePressed( + List<(WalletFeature, String, FutureOr Function())> options, + ) async { await showDialog( context: context, builder: - (_) => MoreFeaturesDialog( - walletId: widget.walletId, - onPaynymPressed: _onPaynymPressed, - onBuyPressed: _onBuyPressed, - onCoinControlPressed: _onCoinControlPressed, - onLelantusCoinsPressed: _onLelantusCoinsPressed, - onSparkCoinsPressedPressed: _onSparkCoinsPressed, - // onAnonymizeAllPressed: _onAnonymizeAllPressed, - onWhirlpoolPressed: _onWhirlpoolPressed, - onOrdinalsPressed: _onOrdinalsPressed, - onMonkeyPressed: _onMonkeyPressed, - onFusionPressed: _onFusionPressed, - onChurnPressed: _onChurnPressed, - onNamesPressed: _onNamesPressed, - ), + (_) => + MoreFeaturesDialog(walletId: widget.walletId, options: options), ); } - void _onWhirlpoolPressed() { - Navigator.of(context, rootNavigator: true).pop(); - } - void _onCoinControlPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopCoinControlView.routeName, arguments: widget.walletId); } void _onLelantusCoinsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(LelantusCoinsView.routeName, arguments: widget.walletId); } void _onSparkCoinsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId); @@ -290,8 +306,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onPaynymPressed() async { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( showDialog( context: context, @@ -332,114 +346,158 @@ class _DesktopWalletFeaturesState extends ConsumerState { } Future _onMonkeyPressed() async { - Navigator.of(context, rootNavigator: true).pop(); - await (Navigator.of( context, ).pushNamed(MonkeyView.routeName, arguments: widget.walletId)); } void _onOrdinalsPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopOrdinalsView.routeName, arguments: widget.walletId); } void _onFusionPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopCashFusionView.routeName, arguments: widget.walletId); } void _onChurnPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(DesktopChurningView.routeName, arguments: widget.walletId); } void _onNamesPressed() { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of( context, ).pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId); } + void _onSparkNamesPressed() { + Navigator.of( + context, + ).pushNamed(SparkNamesHomeView.routeName, arguments: widget.walletId); + } + + List<(WalletFeature, String, FutureOr Function())> _getOptions( + Wallet wallet, + bool showExchange, + bool showCoinControl, + bool firoAdvanced, + ) { + final coin = wallet.info.coin; + final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; + + return [ + if (!isViewOnly && coin is Firo) + ( + WalletFeature.anonymizeFunds, + Assets.svg.recycle, + _onAnonymizeAllPressed, + ), + if (!isViewOnly && + Constants.enableExchange && + AppConfig.hasFeature(AppFeature.swap) && + showExchange) + (WalletFeature.swap, Assets.svg.swap, _onSwapPressed), + + if (showExchange && AppConfig.hasFeature(AppFeature.buy)) + (WalletFeature.buy, Assets.svg.swap, _onBuyPressed), + + if (wallet is SparkInterface) + (WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed), + + if (showCoinControl) + ( + WalletFeature.coinControl, + Assets.svg.coinControl.gamePad, + _onCoinControlPressed, + ), + + if (firoAdvanced && wallet is FiroWallet) + ( + WalletFeature.lelantusCoins, + Assets.svg.coinControl.gamePad, + _onLelantusCoinsPressed, + ), + + if (firoAdvanced && wallet is FiroWallet) + ( + WalletFeature.sparkCoins, + Assets.svg.coinControl.gamePad, + _onSparkCoinsPressed, + ), + + if (!isViewOnly && wallet is PaynymInterface) + (WalletFeature.paynym, Assets.svg.robotHead, _onPaynymPressed), + + if (wallet is OrdinalsInterface) + (WalletFeature.ordinals, Assets.svg.ordinal, _onOrdinalsPressed), + + if (wallet.info.coin is Banano) + (WalletFeature.monkey, Assets.svg.monkey, _onMonkeyPressed), + + if (!isViewOnly && wallet is CashFusionInterface) + (WalletFeature.fusion, Assets.svg.cashFusion, _onFusionPressed), + + if (!isViewOnly && wallet is LibMoneroWallet) + (WalletFeature.churn, Assets.svg.churn, _onChurnPressed), + + if (wallet is NamecoinWallet) + (WalletFeature.namecoinName, Assets.svg.robotHead, _onNamesPressed), + ]; + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); - final coin = wallet.info.coin; - final prefs = ref.watch(prefsChangeNotifierProvider); - final showExchange = prefs.enableExchange; - - final showMore = - wallet is PaynymInterface || - (wallet is CoinControlInterface && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.enableCoinControl, - ), - )) || - coin is Firo || - // manager.hasWhirlpoolSupport || - coin is Banano || - wallet is OrdinalsInterface || - wallet is CashFusionInterface; + final options = _getOptions( + wallet, + ref.watch( + prefsChangeNotifierProvider.select((value) => value.enableExchange), + ), + (wallet is CoinControlInterface && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + )), + ref.watch( + prefsChangeNotifierProvider.select((s) => s.advancedFiroFeatures), + ), + ); final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isViewOnly && wallet.info.coin is Firo) - SecondaryButton( - label: "Anonymize funds", - width: buttonWidth * 2, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.recycle, - height: 20, - width: 20, - color: - Theme.of( - context, - ).extension()!.buttonTextSecondary, - ), - onPressed: () => _onAnonymizeAllPressed(), - ), - if (!isViewOnly && wallet.info.coin is Firo) const SizedBox(width: 16), - if (!isViewOnly && - Constants.enableExchange && - AppConfig.hasFeature(AppFeature.swap) && - showExchange) - SecondaryButton( - label: "Swap", - width: buttonWidth, - buttonHeight: ButtonHeight.l, - icon: SvgPicture.asset( - Assets.svg.arrowRotate, - height: 20, - width: 20, - color: - Theme.of( - context, - ).extension()!.buttonTextSecondary, - ), - onPressed: () => _onSwapPressed(), - ), + final isViewOnlyNoAddressGen = + wallet is ViewOnlyOptionInterface && + wallet.isViewOnly && + wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; + + final extraOptions = [ + if (wallet is SparkInterface && !isViewOnly) + (WalletFeature.clearSparkCache, Assets.svg.key, () => ()), + + if (wallet is LelantusInterface && !isViewOnly) + (WalletFeature.lelantusScanOption, Assets.svg.key, () => ()), - if (showMore) const SizedBox(width: 16), - if (showMore) - SecondaryButton( + if (wallet is RbfInterface) (WalletFeature.rbf, Assets.svg.key, () => ()), + + if (!isViewOnlyNoAddressGen) + (WalletFeature.reuseAddress, Assets.svg.key, () => ()), + ]; + + return StaticOverflowRow( + forcedOverflow: extraOptions.isNotEmpty, + overflowBuilder: (count) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SecondaryButton( label: "More", - width: buttonWidth, + padding: const EdgeInsets.symmetric(horizontal: 16), buttonHeight: ButtonHeight.l, icon: SvgPicture.asset( Assets.svg.bars, @@ -450,9 +508,52 @@ class _DesktopWalletFeaturesState extends ConsumerState { context, ).extension()!.buttonTextSecondary, ), - onPressed: () => _onMorePressed(), + onPressed: + () => _onMorePressed([ + ...options.sublist(options.length - count), + ...extraOptions, + ]), ), - ], + ); + }, + + children: options + .map( + (option) => Padding( + padding: const EdgeInsets.only(left: 16), + child: SecondaryButton( + label: option.$1.label, + padding: const EdgeInsets.symmetric(horizontal: 16), + buttonHeight: ButtonHeight.l, + icon: + option.$1 == WalletFeature.buy + ? SvgPicture.file( + File( + ref.watch( + themeProvider.select((value) => value.assets.buy), + ), + ), + height: 20, + width: 20, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, + ) + : SvgPicture.asset( + option.$2, + height: 20, + width: 20, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, + ), + onPressed: () => option.$3(), + ), + ), + ) + .toList(growable: false), ); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 1ee0447b3..5a013787a 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -8,6 +8,7 @@ * */ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -15,16 +16,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import '../../../../../app_config.dart'; import '../../../../../db/sqlite/firo_cache.dart'; -import '../../../../../models/keys/view_only_wallet_data.dart'; import '../../../../../providers/db/main_db_provider.dart'; -import '../../../../../providers/global/prefs_provider.dart'; import '../../../../../providers/global/wallets_provider.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/theme_providers.dart'; import '../../../../../utilities/assets.dart'; -import '../../../../../utilities/constants.dart'; import '../../../../../utilities/logger.dart'; import '../../../../../utilities/show_loading.dart'; import '../../../../../utilities/text_styles.dart'; @@ -32,17 +29,6 @@ import '../../../../../utilities/util.dart'; import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../../wallets/wallet/impl/firo_wallet.dart'; -import '../../../../../wallets/wallet/impl/namecoin_wallet.dart'; -import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/ordinals_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import '../../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; @@ -50,38 +36,17 @@ import '../../../../../widgets/desktop/primary_button.dart'; import '../../../../../widgets/desktop/secondary_button.dart'; import '../../../../../widgets/rounded_container.dart'; import '../../../../../widgets/stack_dialog.dart'; +import '../desktop_wallet_features.dart'; class MoreFeaturesDialog extends ConsumerStatefulWidget { const MoreFeaturesDialog({ super.key, required this.walletId, - required this.onPaynymPressed, - required this.onBuyPressed, - required this.onCoinControlPressed, - required this.onLelantusCoinsPressed, - required this.onSparkCoinsPressedPressed, - // required this.onAnonymizeAllPressed, - required this.onWhirlpoolPressed, - required this.onOrdinalsPressed, - required this.onMonkeyPressed, - required this.onFusionPressed, - required this.onChurnPressed, - required this.onNamesPressed, + required this.options, }); final String walletId; - final VoidCallback? onPaynymPressed; - final VoidCallback? onBuyPressed; - final VoidCallback? onCoinControlPressed; - final VoidCallback? onLelantusCoinsPressed; - final VoidCallback? onSparkCoinsPressedPressed; - // final VoidCallback? onAnonymizeAllPressed; - final VoidCallback? onWhirlpoolPressed; - final VoidCallback? onOrdinalsPressed; - final VoidCallback? onMonkeyPressed; - final VoidCallback? onFusionPressed; - final VoidCallback? onChurnPressed; - final VoidCallback? onNamesPressed; + final List<(WalletFeature, String, FutureOr Function())> options; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -364,16 +329,6 @@ class _MoreFeaturesDialogState extends ConsumerState { pWallets.select((value) => value.getWallet(widget.walletId)), ); - final coinControlPrefEnabled = ref.watch( - prefsChangeNotifierProvider.select((value) => value.enableCoinControl), - ); - - final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; - final isViewOnlyNoAddressGen = - wallet is ViewOnlyOptionInterface && - wallet.isViewOnly && - wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; - return DesktopDialog( maxHeight: double.infinity, child: Column( @@ -392,213 +347,147 @@ class _MoreFeaturesDialogState extends ConsumerState { const DesktopDialogCloseButton(), ], ), - if (Constants.enableExchange && - AppConfig.hasFeature(AppFeature.buy) && - ref.watch(prefsChangeNotifierProvider).enableExchange) - _MoreFeaturesItem( - label: "Buy", - detail: "Buy cryptocurrency", - isSvgFile: true, - iconAsset: ref.watch( - themeProvider.select((value) => value.assets.buy), - ), - onPressed: () async => widget.onBuyPressed?.call(), - ), - // if (!isViewOnly && wallet.info.coin is Firo) - // _MoreFeaturesItem( - // label: "Anonymize funds", - // detail: "Anonymize funds", - // iconAsset: Assets.svg.recycle, - // onPressed: () async => widget.onAnonymizeAllPressed?.call(), - // ), - // TODO: [prio=med] - // if (manager.hasWhirlpoolSupport) - // _MoreFeaturesItem( - // label: "Whirlpool", - // detail: "Powerful Bitcoin privacy enhancer", - // iconAsset: Assets.svg.whirlPool, - // onPressed: () => widget.onWhirlpoolPressed?.call(), - // ), - if (wallet is CoinControlInterface && coinControlPrefEnabled) - _MoreFeaturesItem( - label: "Coin control", - detail: "Control, freeze, and utilize outputs at your discretion", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onCoinControlPressed?.call(), - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (s) => s.advancedFiroFeatures, - ), - )) - _MoreFeaturesItem( - label: "Lelantus Coins", - detail: "View wallet lelantus coins", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onLelantusCoinsPressed?.call(), - ), - if (wallet is FiroWallet && - ref.watch( - prefsChangeNotifierProvider.select( - (s) => s.advancedFiroFeatures, - ), - )) - _MoreFeaturesItem( - label: "Spark Coins", - detail: "View wallet spark coins", - iconAsset: Assets.svg.coinControl.gamePad, - onPressed: () async => widget.onSparkCoinsPressedPressed?.call(), - ), - if (!isViewOnly && wallet is PaynymInterface) - _MoreFeaturesItem( - label: "PayNym", - detail: "Increased address privacy using BIP47", - iconAsset: Assets.svg.robotHead, - onPressed: () async => widget.onPaynymPressed?.call(), - ), - if (wallet is OrdinalsInterface) - _MoreFeaturesItem( - label: "Ordinals", - detail: "View and control your ordinals in ${AppConfig.prefix}", - iconAsset: Assets.svg.ordinal, - onPressed: () async => widget.onOrdinalsPressed?.call(), - ), - if (wallet.info.coin is Banano) - _MoreFeaturesItem( - label: "MonKey", - detail: "Generate Banano MonKey", - iconAsset: Assets.svg.monkey, - onPressed: () async => widget.onMonkeyPressed?.call(), - ), - if (!isViewOnly && wallet is CashFusionInterface) - _MoreFeaturesItem( - label: "Fusion", - detail: "Decentralized mixing protocol", - iconAsset: Assets.svg.cashFusion, - onPressed: () async => widget.onFusionPressed?.call(), - ), - if (!isViewOnly && wallet is LibMoneroWallet) - _MoreFeaturesItem( - label: "Churn", - detail: "Churning", - iconAsset: Assets.svg.churn, - onPressed: () async => widget.onChurnPressed?.call(), - ), - if (wallet is NamecoinWallet) - _MoreFeaturesItem( - label: "Domains", - detail: "Namecoin DNS", - iconAsset: Assets.svg.robotHead, - onPressed: () async => widget.onNamesPressed?.call(), - ), - if (wallet is SparkInterface && !isViewOnly) - _MoreFeaturesClearSparkCacheItem( - cryptoCurrency: wallet.cryptoCurrency, - ), - if (wallet is LelantusInterface && !isViewOnly) - _MoreFeaturesItemBase( - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.enableLelantusScanning] - as bool? ?? - false, - onValueChanged: _switchToggled, - ), + + ...widget.options.map((option) { + switch (option.$1) { + case WalletFeature.buy: + // Buy has a special icon + return _MoreFeaturesItem( + label: option.$1.label, + detail: option.$1.description, + isSvgFile: true, + iconAsset: ref.watch( + themeProvider.select((value) => value.assets.buy), ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + onPressed: () async { + Navigator.of(context, rootNavigator: true).pop(); + option.$3(); + }, + ); + + case WalletFeature.clearSparkCache: + return _MoreFeaturesClearSparkCacheItem( + cryptoCurrency: wallet.cryptoCurrency, + ); + + case WalletFeature.lelantusScanOption: + return _MoreFeaturesItemBase( + child: Row( children: [ - Text( - "Scan for Lelantus transactions", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.enableLelantusScanning] + as bool? ?? + false, + onValueChanged: _switchToggled, + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Scan for Lelantus transactions", + style: STextStyles.w600_20(context), + ), + ], ), ], ), - ], - ), - ), - if (wallet is RbfInterface) - _MoreFeaturesItemBase( - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.enableOptInRbf] - as bool? ?? - false, - onValueChanged: _switchRbfToggled, - ), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + + case WalletFeature.rbf: + return _MoreFeaturesItemBase( + child: Row( children: [ - Text( - "Flag outgoing transactions with opt-in RBF", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.enableOptInRbf] + as bool? ?? + false, + onValueChanged: _switchRbfToggled, + ), ), - ], - ), - ], - ), - ), - // reuseAddress preference. - if (!isViewOnlyNoAddressGen) - _MoreFeaturesItemBase( - onPressed: _switchReuseAddressToggled, - child: Row( - children: [ - const SizedBox(width: 3), - SizedBox( - height: 20, - width: 40, - child: IgnorePointer( - child: DraggableSwitchButton( - isOn: - ref.watch( - pWalletInfo( - widget.walletId, - ).select((value) => value.otherData), - )[WalletInfoKeys.reuseAddress] - as bool? ?? - false, - controller: _switchController, + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Flag outgoing transactions with opt-in RBF", + style: STextStyles.w600_20(context), + ), + ], ), - ), + ], ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + ); + + case WalletFeature.reuseAddress: + return _MoreFeaturesItemBase( + onPressed: _switchReuseAddressToggled, + child: Row( children: [ - Text( - "Reuse receiving address", - style: STextStyles.w600_20(context), + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select((value) => value.otherData), + )[WalletInfoKeys.reuseAddress] + as bool? ?? + false, + controller: _switchController, + ), + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Reuse receiving address", + style: STextStyles.w600_20(context), + ), + ], ), ], ), - ], - ), - ), + ); + + default: + return _MoreFeaturesItem( + label: option.$1.label, + detail: option.$1.description, + iconAsset: option.$2, + onPressed: () async { + Navigator.of(context, rootNavigator: true).pop(); + option.$3(); + }, + ); + } + }), + const SizedBox(height: 28), ], ), diff --git a/lib/providers/db/drift_provider.dart b/lib/providers/db/drift_provider.dart new file mode 100644 index 000000000..658dd5bc7 --- /dev/null +++ b/lib/providers/db/drift_provider.dart @@ -0,0 +1,17 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2025 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2025-05-06 + * + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../db/drift/database.dart'; + +final pDrift = Provider.family( + (ref, walletId) => Drift.get(walletId), +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index e6e9ef20b..0f2f12343 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -11,6 +11,8 @@ export './buy/buy_form_state_provider.dart'; export './buy/simplex_initial_load_status.dart'; export './buy/simplex_provider.dart'; +export './db/drift_provider.dart'; +export './db/main_db_provider.dart'; export './exchange/changenow_initial_load_status.dart'; export './exchange/exchange_flow_is_active_state_provider.dart'; export './exchange/exchange_form_state_provider.dart'; @@ -25,6 +27,7 @@ export './global/price_provider.dart'; export './global/should_show_lockscreen_on_resume_state_provider.dart'; export './global/wallets_provider.dart'; export './global/wallets_service_provider.dart'; +export './progress_report/xelis_table_progress_provider.dart'; export './ui/add_wallet_selected_coin_provider.dart'; export './ui/check_box_state_provider.dart'; export './ui/home_view_index_provider.dart'; @@ -32,4 +35,3 @@ export './ui/verify_recovery_phrase/correct_word_provider.dart'; export './ui/verify_recovery_phrase/random_index_provider.dart'; export './ui/verify_recovery_phrase/selected_word_provider.dart'; export './wallet/transaction_note_provider.dart'; -export './progress_report/xelis_table_progress_provider.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index eaa8c1c06..fb0e79c86 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -13,6 +13,7 @@ import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; +import 'db/drift/database.dart'; import 'models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'models/buy/response_objects/quote.dart'; @@ -147,6 +148,9 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; +import 'pages/spark_names/buy_spark_name_view.dart'; +import 'pages/spark_names/spark_names_home_view.dart'; +import 'pages/spark_names/sub_widgets/spark_name_details.dart'; import 'pages/special/firo_rescan_recovery_error_dialog.dart'; import 'pages/stack_privacy_calls.dart'; import 'pages/token_view/my_tokens_view.dart'; @@ -253,12 +257,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePinView( - popOnSuccess: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePinView(popOnSuccess: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -285,14 +285,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseCoinView( - title: args.item1, - coinAdditional: args.item2, - nextRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ChooseCoinView( + title: args.item1, + coinAdditional: args.item2, + nextRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -301,12 +300,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageExplorerView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ManageExplorerView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -315,12 +310,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FiroRescanRecoveryErrorView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FiroRescanRecoveryErrorView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -343,23 +334,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditWalletTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args.item1, - contractsToMarkSelected: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditWalletTokensView( + walletId: args.item1, + contractsToMarkSelected: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -368,12 +354,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopTokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopTokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -382,12 +364,8 @@ class RouteGenerator { if (args is EthTokenEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectWalletForTokenView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SelectWalletForTokenView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -396,21 +374,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const AddCustomTokenView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletsOverview.routeName: if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletsOverview( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletsOverview(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -419,13 +391,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenContractDetailsView( - contractAddress: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenContractDetailsView( + contractAddress: args.item1, + walletId: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -434,13 +405,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SingleFieldEditView( - initialValue: args.item1, - label: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SingleFieldEditView( + initialValue: args.item1, + label: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -449,66 +419,50 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MonkeyView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MonkeyView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case CreateNewFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateNewFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CreateNewFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case SelectNewFrostImportTypeView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectNewFrostImportTypeView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SelectNewFrostImportTypeView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -517,21 +471,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const FrostStepScaffold(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case FrostMSWalletOptionsView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostMSWalletOptionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostMSWalletOptionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -540,12 +488,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostParticipantsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostParticipantsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -554,12 +498,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => InitiateResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => InitiateResharingView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -568,31 +508,23 @@ class RouteGenerator { if (args is ({String walletId, Map resharers})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CompleteReshareConfigView( - walletId: args.walletId, - resharers: args.resharers, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CompleteReshareConfigView( + walletId: args.walletId, + resharers: args.resharers, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FrostSendView.routeName: - if (args is ({ - String walletId, - CryptoCurrency coin, - })) { + if (args is ({String walletId, CryptoCurrency coin})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostSendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => FrostSendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -616,27 +548,22 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView(walletId: args.item1, type: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple4?>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - requestedTotal: args.item3, - selectedUTXOs: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView( + walletId: args.item1, + type: args.item2, + requestedTotal: args.item3, + selectedUTXOs: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -645,12 +572,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => OrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -659,12 +582,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopOrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -673,13 +592,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => OrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -688,13 +606,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DesktopOrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -710,13 +627,10 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => UtxoDetailsView( - walletId: args.item2, - utxoId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + UtxoDetailsView(walletId: args.item2, utxoId: args.item1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -725,13 +639,8 @@ class RouteGenerator { if (args is (Id, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameDetailsView( - walletId: args.$2, - utxoId: args.$1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NameDetailsView(walletId: args.$2, utxoId: args.$1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -740,12 +649,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymClaimView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymClaimView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -754,12 +659,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -768,12 +669,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewPaynymFollowView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewPaynymFollowView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -782,12 +679,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -796,12 +689,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NamecoinNamesHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NamecoinNamesHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -810,13 +699,158 @@ class RouteGenerator { if (args is ({String walletId, UTXO utxo})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageDomainView( - walletId: args.walletId, - utxo: args.utxo, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + ManageDomainView(walletId: args.walletId, utxo: args.utxo), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + case SparkNamesHomeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SparkNamesHomeView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case BuySparkNameView.routeName: + if (args is ({String walletId, String name})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => + BuySparkNameView(walletId: args.walletId, name: args.name), + settings: RouteSettings(name: settings.name), + ); + } else if (args + is ({String walletId, String name, SparkName? nameToRenew})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => BuySparkNameView( + walletId: args.walletId, + name: args.name, + nameToRenew: args.nameToRenew, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case SparkNameDetailsView.routeName: + if (args is ({String walletId, SparkName name})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => SparkNameDetailsView( + walletId: args.walletId, + name: args.name, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -825,12 +859,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FusionProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -839,12 +869,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -853,12 +879,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -867,12 +889,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -881,12 +899,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -902,12 +916,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressBookView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddressBookView(coin: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -1011,13 +1021,8 @@ class RouteGenerator { if (args is (String, ({List xpubs, String fingerprint}))) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => XPubView( - walletId: args.$1, - xpubData: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => XPubView(walletId: args.$1, xpubData: args.$2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1026,12 +1031,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChangeRepresentativeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChangeRepresentativeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1117,12 +1118,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFromEncryptedStringView( - encrypted: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RestoreFromEncryptedStringView(encrypted: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1138,12 +1135,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditCoinUnitsView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditCoinUnitsView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1173,12 +1166,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinNodesView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CoinNodesView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1187,14 +1176,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NodeDetailsView( - coin: args.item1, - nodeId: args.item2, - popRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NodeDetailsView( + coin: args.item1, + nodeId: args.item2, + popRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1203,13 +1191,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditNoteView( - txid: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditNoteView(txid: args.item1, walletId: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1218,12 +1202,8 @@ class RouteGenerator { if (args is int) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditAddressLabelView( - addressLabelId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditAddressLabelView(addressLabelId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1232,13 +1212,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditTradeNoteView( - tradeId: args.item1, - note: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditTradeNoteView(tradeId: args.item1, note: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1248,15 +1224,14 @@ class RouteGenerator { is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddEditNodeView( - viewType: args.item1, - coin: args.item2, - nodeId: args.item3, - routeOnSuccessOrDelete: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddEditNodeView( + viewType: args.item1, + coin: args.item2, + nodeId: args.item3, + routeOnSuccessOrDelete: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1265,12 +1240,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ContactDetailsView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ContactDetailsView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1279,12 +1250,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewContactAddressView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewContactAddressView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1293,12 +1260,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactNameEmojiView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditContactNameEmojiView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1307,13 +1270,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactAddressView( - contactId: args.item1, - addressEntry: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditContactAddressView( + contactId: args.item1, + addressEntry: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1322,23 +1284,20 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const SystemBrightnessThemeSelectionView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletNetworkSettingsView.routeName: if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletNetworkSettingsView( - walletId: args.item1, - initialSyncStatus: args.item2, - initialNodeStatus: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletNetworkSettingsView( + walletId: args.item1, + initialSyncStatus: args.item2, + initialNodeStatus: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1347,91 +1306,88 @@ class RouteGenerator { if (args is ({String walletId, List mnemonic})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List 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, - ), + } else if (args + is ({ + String walletId, + List 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), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - ({ - 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, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + ({ + 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, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case MobileKeyDataView.routeName: - if (args is ({ - String walletId, - KeyDataInterface keyData, - })) { + if (args is ({String walletId, KeyDataInterface keyData})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MobileKeyDataView( - walletId: args.walletId, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => MobileKeyDataView( + walletId: args.walletId, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1440,12 +1396,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsWalletSettingsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletSettingsWalletSettingsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1454,12 +1406,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RenameWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RenameWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1468,12 +1416,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletWarningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeleteWalletWarningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1482,12 +1426,8 @@ class RouteGenerator { if (args is AddWalletListEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateOrRestoreWalletView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreateOrRestoreWalletView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1496,13 +1436,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameYourWalletView( - addWalletType: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NameYourWalletView( + addWalletType: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1511,13 +1450,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseWarningView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseWarningView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1526,13 +1464,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1541,55 +1478,52 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreWalletView.routeName: - if (args - is Tuple6) { + if (args is Tuple6) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreWalletView( - walletName: args.item1, - coin: args.item2, - seedWordsLength: args.item3, - restoreBlockHeight: args.item4, - mnemonicPassphrase: args.item5, - enableLelantusScanning: args.item6 ?? false, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreWalletView( + walletName: args.item1, + coin: args.item2, + seedWordsLength: args.item3, + restoreBlockHeight: args.item4, + mnemonicPassphrase: args.item5, + enableLelantusScanning: args.item6 ?? false, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreViewOnlyWalletView.routeName: - if (args is ({ - String walletName, - CryptoCurrency coin, - int restoreBlockHeight, - bool enableLelantusScanning, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreViewOnlyWalletView( - walletName: args.walletName, - coin: args.coin, - restoreBlockHeight: args.restoreBlockHeight, - enableLelantusScanning: args.enableLelantusScanning, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + String walletName, + CryptoCurrency coin, + int restoreBlockHeight, + bool enableLelantusScanning, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => RestoreViewOnlyWalletView( + walletName: args.walletName, + coin: args.coin, + restoreBlockHeight: args.restoreBlockHeight, + enableLelantusScanning: args.enableLelantusScanning, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1598,13 +1532,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1613,13 +1546,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => VerifyRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => VerifyRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1634,12 +1566,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1648,54 +1576,49 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionDetailsView( - transaction: args.item1, - coin: args.item2, - walletId: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionDetailsView( + transaction: args.item1, + coin: args.item2, + walletId: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case TransactionV2DetailsView.routeName: - if (args is ({ - TransactionV2 tx, - CryptoCurrency coin, - String walletId - })) { + if (args + is ({TransactionV2 tx, CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionV2DetailsView( - transaction: args.tx, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionV2DetailsView( + transaction: args.tx, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FusionGroupDetailsView.routeName: - if (args is ({ - List transactions, - CryptoCurrency coin, - String walletId - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionGroupDetailsView( - transactions: args.transactions, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + List transactions, + CryptoCurrency coin, + String walletId, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => FusionGroupDetailsView( + transactions: args.transactions, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1704,12 +1627,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1718,24 +1637,19 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsV2View(walletId: args), + settings: RouteSettings(name: settings.name), ); } if (args is ({String walletId, String contractAddress})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args.walletId, - contractAddress: args.contractAddress, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AllTransactionsV2View( + walletId: args.walletId, + contractAddress: args.contractAddress, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1744,12 +1658,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionSearchFilterView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TransactionSearchFilterView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1758,23 +1668,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ReceiveView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args.item1, - tokenContract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ReceiveView( + walletId: args.item1, + tokenContract: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1783,12 +1688,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1797,13 +1698,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressDetailsView( - walletId: args.item2, - addressId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddressDetailsView( + walletId: args.item2, + addressId: args.item1, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1812,49 +1712,37 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.item1, coin: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - autoFillData: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + autoFillData: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - accountLite: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + accountLite: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is ({CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } @@ -1864,14 +1752,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenSendView( - walletId: args.item1, - coin: args.item2, - tokenContract: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenSendView( + walletId: args.item1, + coin: args.item2, + tokenContract: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1880,14 +1767,13 @@ class RouteGenerator { if (args is (TxData, String, VoidCallback)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: args.$1, - walletId: args.$2, - onSuccess: args.$3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmTransactionView( + txData: args.$1, + walletId: args.$2, + onSuccess: args.$3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1896,13 +1782,12 @@ class RouteGenerator { if (args is (TxData, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmNameTransactionView( - txData: args.$1, - walletId: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmNameTransactionView( + txData: args.$1, + walletId: args.$2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1911,40 +1796,38 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, - contract: args.item3, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + contract: args.item3, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1953,30 +1836,30 @@ class RouteGenerator { if (args is String?) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NotificationsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NotificationsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case WalletSettingsView.routeName: - if (args is Tuple4) { + if (args + is Tuple4< + String, + CryptoCurrency, + WalletSyncStatus, + NodeConnectionStatus + >) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsView( - walletId: args.item1, - coin: args.item2, - initialSyncStatus: args.item3, - initialNodeStatus: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletSettingsView( + walletId: args.item1, + coin: args.item2, + initialSyncStatus: args.item3, + initialNodeStatus: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1985,34 +1868,34 @@ class RouteGenerator { if (args is ({String walletId, List mnemonicWords})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonicWords, - ({ - String myName, - String config, - String keys, - ({String config, String keys})? prevGen, - })? frostWalletData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - frostWalletData: args.frostWalletData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonicWords, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + frostWalletData: args.frostWalletData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2021,13 +1904,12 @@ class RouteGenerator { if (args is ({String walletId, ViewOnlyWalletData data})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteViewOnlyWalletKeysView( - data: args.data, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteViewOnlyWalletKeysView( + data: args.data, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2038,12 +1920,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step1View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step1View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2052,12 +1930,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step2View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step2View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2066,12 +1940,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step3View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step3View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2080,12 +1950,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step4View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step4View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2094,15 +1960,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TradeDetailsView( - tradeId: args.item1, - transactionIfSentFromStack: args.item2, - walletId: args.item3, - walletName: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TradeDetailsView( + tradeId: args.item1, + transactionIfSentFromStack: args.item2, + walletId: args.item3, + walletName: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2111,12 +1976,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseFromStackView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChooseFromStackView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2125,15 +1986,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendFromView( - coin: args.item1, - amount: args.item2, - trade: args.item4, - address: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendFromView( + coin: args.item1, + amount: args.item2, + trade: args.item4, + address: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2142,13 +2002,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: args.item1, - receivingAddress: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2157,12 +2016,8 @@ class RouteGenerator { if (args is SimplexQuote) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyQuotePreviewView( - quote: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BuyQuotePreviewView(quote: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2172,9 +2027,7 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => LelantusSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2184,9 +2037,7 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RbfSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2195,12 +2046,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkInfoView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkInfoView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2209,12 +2056,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditRefreshHeightView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditRefreshHeightView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2223,13 +2066,12 @@ class RouteGenerator { if (args is ({String walletId, String domainName})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyDomainView( - walletId: args.walletId, - domainName: args.domainName, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyDomainView( + walletId: args.walletId, + domainName: args.domainName, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2239,12 +2081,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePasswordView( - restoreFromSWB: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePasswordView(restoreFromSWB: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -2271,12 +2109,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeletePasswordWarningView( - shouldCreateNew: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeletePasswordWarningView(shouldCreateNew: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2314,21 +2148,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => BuyInWalletView(coin: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyInWalletView( - coin: args.item1, - contract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyInWalletView(coin: args.item1, contract: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2365,12 +2193,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2379,12 +2203,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2393,12 +2213,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => LelantusCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => LelantusCoinsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2407,12 +2223,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkCoinsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2421,12 +2233,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCoinControlView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCoinControlView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2435,12 +2243,8 @@ class RouteGenerator { if (args is TransactionV2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BoostTransactionView( - transaction: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BoostTransactionView(transaction: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2530,27 +2334,27 @@ class RouteGenerator { ); case WalletKeysDesktopPopup.routeName: - if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData - })) { + if (args + is ({ + List mnemonic, + String walletId, + ({String keys, String config})? frostData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, frostData: args.frostData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + ({String keys, String config})? frostData, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, @@ -2558,24 +2362,21 @@ class RouteGenerator { frostData: args.frostData, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2583,12 +2384,8 @@ class RouteGenerator { case UnlockWalletKeysDesktop.routeName: if (args is String) { return FadePageRoute( - UnlockWalletKeysDesktop( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + UnlockWalletKeysDesktop(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2605,12 +2402,8 @@ class RouteGenerator { case DesktopDeleteWalletDialog.routeName: if (args is String) { return FadePageRoute( - DesktopDeleteWalletDialog( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopDeleteWalletDialog(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2627,12 +2420,8 @@ class RouteGenerator { case DesktopAttentionDeleteWallet.routeName: if (args is String) { return FadePageRoute( - DesktopAttentionDeleteWallet( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopAttentionDeleteWallet(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2649,13 +2438,8 @@ class RouteGenerator { case DeleteWalletKeysPopup.routeName: if (args is Tuple2>) { return FadePageRoute( - DeleteWalletKeysPopup( - walletId: args.item1, - words: args.item2, - ), - RouteSettings( - name: settings.name, - ), + DeleteWalletKeysPopup(walletId: args.item1, words: args.item2), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2672,12 +2456,8 @@ class RouteGenerator { case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( - QRCodeDesktopPopupContent( - value: args, - ), - RouteSettings( - name: settings.name, - ), + QRCodeDesktopPopupContent(value: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2695,12 +2475,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MyTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MyTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2723,23 +2499,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is ({String walletId, bool popPrevious})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args.walletId, - popPrevious: args.popPrevious, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenView( + walletId: args.walletId, + popPrevious: args.popPrevious, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2785,13 +2556,12 @@ class RouteGenerator { final end = Offset.zero; final curve = Curves.easeInOut; - final tween = - Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + final tween = Tween( + begin: begin, + end: end, + ).chain(CurveTween(curve: curve)); - return SlideTransition( - position: animation.drive(tween), - child: child, - ); + return SlideTransition(position: animation.drive(tween), child: child); }, ); } @@ -2835,10 +2605,7 @@ class FadePageRoute extends PageRoute { Animation animation, Animation secondaryAnimation, ) { - return FadeTransition( - opacity: animation, - child: child, - ); + return FadeTransition(opacity: animation, child: child); } @override diff --git a/lib/services/spark_names_service.dart b/lib/services/spark_names_service.dart new file mode 100644 index 000000000..9a9a4aa96 --- /dev/null +++ b/lib/services/spark_names_service.dart @@ -0,0 +1,126 @@ +import 'package:mutex/mutex.dart'; + +import '../utilities/logger.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; + +class _BiMap { + final Map _byKey = {}; + final Map _byValue = {}; + + _BiMap(); + + void addAll(Map other) { + for (final e in other.entries) { + add(e.key, e.value); + } + } + + void add(K key, V value) { + _byKey[key] = value; + _byValue[value] = key; + } + + void clear() { + _byValue.clear(); + _byKey.clear(); + } + + K? getByValue(V value) => _byValue[value]; + V? getByKey(K key) => _byKey[key]; +} + +/// Basic service to track all spark names on test net and main net. +/// Data is currently stored in memory only. +abstract final class SparkNamesService { + static final _lock = { + CryptoCurrencyNetwork.main: Mutex(), + CryptoCurrencyNetwork.test: Mutex(), + }; + + static const _minUpdateInterval = Duration(seconds: 10); + static DateTime _lastUpdated = DateTime(2000); // some default + + static final _cache = { + // key is address, uppercase name is value + CryptoCurrencyNetwork.main: _BiMap(), + CryptoCurrencyNetwork.test: _BiMap(), + }; + + static final _nameMap = { + // key is uppercase, value is as entered + CryptoCurrencyNetwork.main: {}, + CryptoCurrencyNetwork.test: {}, + }; + + /// Get the address for the given spark name. + static Future getAddressFor( + String name, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + return await _lock[network]!.protect( + () async => _cache[network]?.getByValue(name.toUpperCase()), + ); + } + + /// Get the name for the given spark address. + static Future getNameFor( + String address, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + return await _lock[network]!.protect( + () async => _nameMap[network]![_cache[network]?.getByKey(address)], + ); + } + + static Future update( + List<({String name, String address})> names, { + CryptoCurrencyNetwork network = CryptoCurrencyNetwork.main, + }) async { + Logging.instance.t("SparkNamesService.update called"); + if (_cache[network] == null) { + throw UnsupportedError( + "CryptoCurrencyNetwork \"${network.name}\" is not currently allowed.", + ); + } + + final now = DateTime.now(); + if (now.difference(_lastUpdated) > _minUpdateInterval) { + _lastUpdated = now; + } else { + Logging.instance.t( + "SparkNamesService.update called too soon. Returning early.", + ); + // too soon, return; + return; + } + + await _lock[network]!.protect(() async { + Logging.instance.t( + "SparkNamesService.update lock acquired and updating cache", + ); + _cache[network]!.clear(); + _nameMap[network]!.clear(); + + for (final pair in names) { + final upperName = pair.name.toUpperCase(); + _nameMap[network]![upperName] = pair.name; + + _cache[network]!.add(pair.address, upperName); + } + + Logging.instance.t("SparkNamesService.update updating cache complete"); + }); + } +} diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 795ea9720..14cb0fae0 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -96,6 +96,19 @@ abstract class StackFileSystem { } } + static Future applicationDriftDirectory() async { + final root = await applicationRootDirectory(); + if (_createSubDirs) { + final dir = Directory("${root.path}/drift"); + if (!dir.existsSync()) { + await dir.create(); + } + return dir; + } else { + return root; + } + } + // Not used in general now. See applicationFiroCacheSQLiteDirectory() // static Future applicationSQLiteDirectory() async { // final root = await applicationRootDirectory(); diff --git a/lib/wallets/crypto_currency/coins/firo.dart b/lib/wallets/crypto_currency/coins/firo.dart index 26957600d..79ed783f8 100644 --- a/lib/wallets/crypto_currency/coins/firo.dart +++ b/lib/wallets/crypto_currency/coins/firo.dart @@ -236,22 +236,9 @@ class Firo extends Bip39HDCurrency with ElectrumXCurrencyInterface { ); case CryptoCurrencyNetwork.test: - // NodeModel( - // host: "firo-testnet.stackwallet.com", - // port: 50002, - // name: DefaultNodes.defaultName, - // id: _nodeId(Coin.firoTestNet), - // useSSL: true, - // enabled: true, - // coinName: Coin.firoTestNet.name, - // isFailover: true, - // isDown: false, - // ); - - // TODO revert to above eventually return NodeModel( - host: "95.179.164.13", - port: 51002, + host: "firo-testnet.stackwallet.com", + port: 50002, name: DefaultNodes.defaultName, id: DefaultNodes.buildId(this), useSSL: true, diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 94474e390..13430294b 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -64,15 +64,17 @@ class TxData { final tezart.OperationsList? tezosOperationsList; // firo spark specific - final List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? sparkRecipients; + final List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipients; final List? sparkMints; final List? usedSparkCoins; + final ({ + String additionalInfo, + String name, + Address sparkAddress, + int validBlocks, + })? + sparkNameInfo; // xelis specific final String? otherData; @@ -122,6 +124,7 @@ class TxData { this.tempTx, this.ignoreCachedBalanceChecks = false, this.opNameState, + this.sparkNameInfo, }); Amount? get amount { @@ -201,9 +204,10 @@ class TxData { } } - int? get estimatedSatsPerVByte => fee != null && vSize != null - ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() - : null; + int? get estimatedSatsPerVByte => + fee != null && vSize != null + ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() + : null; TxData copyWith({ FeeRateType? feeRateType, @@ -237,19 +241,20 @@ class TxData { TransactionSubType? txSubType, List>? mintsMapLelantus, tezart.OperationsList? tezosOperationsList, - List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? - sparkRecipients, + List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipients, List? sparkMints, List? usedSparkCoins, TransactionV2? tempTx, bool? ignoreCachedBalanceChecks, NameOpState? opNameState, + ({ + String additionalInfo, + String name, + Address sparkAddress, + int validBlocks, + })? + sparkNameInfo, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -290,11 +295,13 @@ class TxData { ignoreCachedBalanceChecks: ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, opNameState: opNameState ?? this.opNameState, + sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, ); } @override - String toString() => 'TxData{' + String toString() => + 'TxData{' 'feeRateType: $feeRateType, ' 'feeRateAmount: $feeRateAmount, ' 'satsPerVByte: $satsPerVByte, ' @@ -331,5 +338,6 @@ class TxData { 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' + 'sparkNameInfo: $sparkNameInfo, ' '}'; } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 347bf9f2c..4768bebb0 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -5,12 +5,14 @@ import 'dart:math'; import 'package:bitcoindart/bitcoindart.dart' as btc; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart' as spark +import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart' + as spark show Log; import 'package:flutter_libsparkmobile/flutter_libsparkmobile.dart'; import 'package:isar/isar.dart'; import 'package:logger/logger.dart'; +import '../../../db/drift/database.dart' show Drift; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -20,11 +22,13 @@ import '../../../models/isar/models/isar_models.dart'; import '../../../models/signing_data.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/spark_names_service.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/prefs.dart'; +import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../isar/models/spark_coin.dart'; import '../../isar/models/wallet_info.dart'; @@ -57,15 +61,10 @@ String _hashTag(String tag) { void initSparkLogging(Level level) { final levels = Level.values.where((e) => e >= level).map((e) => e.name); - spark.Log.levels - .addAll(LoggingLevel.values.where((e) => levels.contains(e.name))); - spark.Log.onLog = ( - level, - value, { - error, - stackTrace, - required time, - }) { + spark.Log.levels.addAll( + LoggingLevel.values.where((e) => levels.contains(e.name)), + ); + spark.Log.onLog = (level, value, {error, stackTrace, required time}) { Logging.instance.log( level.getLoggerLevel(), value, @@ -84,27 +83,24 @@ abstract class _SparkIsolate { static Future initialize() async { final level = Prefs.instance.logLevel; - _isolate = await Isolate.spawn( - (SendPort sendPort) { - initSparkLogging(level); // ensure logging is set up in isolate + _isolate = await Isolate.spawn((SendPort sendPort) { + initSparkLogging(level); // ensure logging is set up in isolate - final receivePort = ReceivePort(); + final receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); + sendPort.send(receivePort.sendPort); - receivePort.listen((message) async { - if (message is List && message.length == 3) { - final function = message[0] as Function; - final argument = message[1]; - final replyPort = message[2] as SendPort; + receivePort.listen((message) async { + if (message is List && message.length == 3) { + final function = message[0] as Function; + final argument = message[1]; + final replyPort = message[2] as SendPort; - final result = await function(argument); - replyPort.send(result); - } - }); - }, - _receivePort.sendPort, - ); + final result = await function(argument); + replyPort.send(result); + } + }); + }, _receivePort.sendPort); _sendPort = await _receivePort.first as SendPort; } @@ -139,8 +135,7 @@ mixin SparkInterface static bool validateSparkAddress({ required String address, required bool isTestNet, - }) => - LibSpark.validateAddress(address: address, isTestNet: isTestNet); + }) => LibSpark.validateAddress(address: address, isTestNet: isTestNet); Future hashTag(String tag) async { try { @@ -192,19 +187,20 @@ mixin SparkInterface @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.spark) - .or() - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.spark) + .or() + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -265,20 +261,22 @@ mixin SparkInterface ); } else { // fetch spendable spark coins - final coins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .and() - .heightIsNotNull() - .and() - .not() - .valueIntStringEqualTo("0") - .findAll(); - - final available = - coins.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + final coins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); + + final available = coins + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e); if (amount.raw > available) { return Amount( @@ -288,16 +286,17 @@ mixin SparkInterface } // prepare coin data for ffi - final serializedCoins = coins - .map( - (e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - ), - ) - .toList(); + final serializedCoins = + coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); final root = await getRootHDNode(); final String derivationPath; @@ -315,6 +314,8 @@ mixin SparkInterface serializedCoins: serializedCoins, // privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), privateRecipientsCount: 1, // ROUGHLY! + utxoNum: 0, // TODO not zero? + additionalTxSize: 0, // spark name script size ); if (estimate < 0) { @@ -329,9 +330,7 @@ mixin SparkInterface } /// Spark to Spark/Transparent (spend) creation - Future prepareSendSpark({ - required TxData txData, - }) async { + Future prepareSendSpark({required TxData txData}) async { // There should be at least one output. if (!(txData.recipients?.isNotEmpty == true || txData.sparkRecipients?.isNotEmpty == true)) { @@ -343,14 +342,15 @@ mixin SparkInterface throw Exception("Spark shielded output limit exceeded."); } - final transparentSumOut = - (txData.recipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e, - ); + final transparentSumOut = (txData.recipients ?? []) + .map((e) => e.amount) + .fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); // See SPARK_VALUE_SPEND_LIMIT_PER_TRANSACTION at https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/include/spark.h#L17 // and COIN https://github.com/firoorg/sparkmobile/blob/ef2e39aae18ecc49e0ddc63a3183e9764b96012e/bitcoin/amount.h#L17 @@ -365,29 +365,31 @@ mixin SparkInterface ); } - final sparkSumOut = - (txData.sparkRecipients ?? []).map((e) => e.amount).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: cryptoCurrency.fractionDigits, - ), - (p, e) => p + e, - ); + final sparkSumOut = (txData.sparkRecipients ?? []) + .map((e) => e.amount) + .fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ), + (p, e) => p + e, + ); final txAmount = transparentSumOut + sparkSumOut; // fetch spendable spark coins - final coins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .and() - .heightIsNotNull() - .and() - .not() - .valueIntStringEqualTo("0") - .findAll(); + final coins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); final available = info.cachedBalanceTertiary.spendable; @@ -398,16 +400,17 @@ mixin SparkInterface final bool isSendAll = available == txAmount; // prepare coin data for ffi - final serializedCoins = coins - .map( - (e) => ( - serializedCoin: e.serializedCoinB64!, - serializedCoinContext: e.contextB64!, - groupId: e.groupId, - height: e.height!, - ), - ) - .toList(); + final serializedCoins = + coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); final currentId = await electrumXClient.getSparkLatestCoinId(); final List> setMaps = []; @@ -433,43 +436,36 @@ mixin SparkInterface "blockHash": info.blockHash, "setHash": info.setHash, "coinGroupID": i, - "coins": resultSet - .map( - (e) => [ - e.serialized, - e.txHash, - e.context, - ], - ) - .toList(), + "coins": + resultSet.map((e) => [e.serialized, e.txHash, e.context]).toList(), }; setData["coinGroupID"] = i; setMaps.add(setData); - idAndBlockHashes.add( - ( - groupId: i, - blockHash: setData["blockHash"] as String, - ), - ); + idAndBlockHashes.add(( + groupId: i, + blockHash: setData["blockHash"] as String, + )); } - final allAnonymitySets = setMaps - .map( - (e) => ( - setId: e["coinGroupID"] as int, - setHash: e["setHash"] as String, - set: (e["coins"] as List) - .map( - (e) => ( - serializedCoin: e[0] as String, - txHash: e[1] as String, - ), - ) - .toList(), - ), - ) - .toList(); + final allAnonymitySets = + setMaps + .map( + (e) => ( + setId: e["coinGroupID"] as int, + setHash: e["setHash"] as String, + set: + (e["coins"] as List) + .map( + (e) => ( + serializedCoin: e[0] as String, + txHash: e[1] as String, + ), + ) + .toList(), + ), + ) + .toList(); final root = await getRootHDNode(); final String derivationPath; @@ -480,31 +476,17 @@ mixin SparkInterface } final privateKey = root.derivePath(derivationPath).privateKey.data; - final txb = btc.TransactionBuilder( - network: _bitcoinDartNetwork, - ); + final txb = btc.TransactionBuilder(network: _bitcoinDartNetwork); txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - List< - ({ - String address, - Amount amount, - bool isChange, - })>? recipientsWithFeeSubtracted; - List< - ({ - String address, - Amount amount, - String memo, - bool isChange, - })>? sparkRecipientsWithFeeSubtracted; - final recipientCount = (txData.recipients - ?.where( - (e) => e.amount.raw > BigInt.zero, - ) - .length ?? - 0); + List<({String address, Amount amount, bool isChange})>? + recipientsWithFeeSubtracted; + List<({String address, Amount amount, String memo, bool isChange})>? + sparkRecipientsWithFeeSubtracted; + final recipientCount = + (txData.recipients?.where((e) => e.amount.raw > BigInt.zero).length ?? + 0); final totalRecipientCount = recipientCount + (txData.sparkRecipients?.length ?? 0); final BigInt estimatedFee; @@ -516,6 +498,8 @@ mixin SparkInterface subtractFeeFromAmount: true, serializedCoins: serializedCoins, privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), + utxoNum: 0, // ?? + additionalTxSize: 0, // name script size ); estimatedFee = BigInt.from(estFee); } else { @@ -530,18 +514,17 @@ mixin SparkInterface } for (int i = 0; i < (txData.sparkRecipients?.length ?? 0); i++) { - sparkRecipientsWithFeeSubtracted!.add( - ( - address: txData.sparkRecipients![i].address, - amount: Amount( - rawValue: txData.sparkRecipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(totalRecipientCount)), - fractionDigits: cryptoCurrency.fractionDigits, - ), - memo: txData.sparkRecipients![i].memo, - isChange: sparkChangeAddress == txData.sparkRecipients![i].address, + sparkRecipientsWithFeeSubtracted!.add(( + address: txData.sparkRecipients![i].address, + amount: Amount( + rawValue: + txData.sparkRecipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(totalRecipientCount)), + fractionDigits: cryptoCurrency.fractionDigits, ), - ); + memo: txData.sparkRecipients![i].memo, + isChange: sparkChangeAddress == txData.sparkRecipients![i].address, + )); } // temp tx data to show in gui while waiting for real data from server @@ -552,17 +535,16 @@ mixin SparkInterface if (txData.recipients![i].amount.raw == BigInt.zero) { continue; } - recipientsWithFeeSubtracted!.add( - ( - address: txData.recipients![i].address, - amount: Amount( - rawValue: txData.recipients![i].amount.raw - - (estimatedFee ~/ BigInt.from(totalRecipientCount)), - fractionDigits: cryptoCurrency.fractionDigits, - ), - isChange: txData.recipients![i].isChange, + recipientsWithFeeSubtracted!.add(( + address: txData.recipients![i].address, + amount: Amount( + rawValue: + txData.recipients![i].amount.raw - + (estimatedFee ~/ BigInt.from(totalRecipientCount)), + fractionDigits: cryptoCurrency.fractionDigits, ), - ); + isChange: txData.recipients![i].isChange, + )); final scriptPubKey = btc.Address.addressToOutputScript( txData.recipients![i].address, @@ -577,10 +559,9 @@ mixin SparkInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: scriptPubKey.toHex, valueStringSats: recipientsWithFeeSubtracted[i].amount.raw.toString(), - addresses: [ - recipientsWithFeeSubtracted[i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [recipientsWithFeeSubtracted[i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -598,10 +579,9 @@ mixin SparkInterface OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: Uint8List.fromList([OP_SPARKSMINT]).toHex, valueStringSats: recip.amount.raw.toString(), - addresses: [ - recip.address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [recip.address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -624,48 +604,112 @@ mixin SparkInterface ); extractedTx.setPayload(Uint8List(0)); - final spend = await computeWithLibSparkLogging( - _createSparkSend, - ( + ({Uint8List script, int size})? noProofNameTxData; + if (txData.sparkNameInfo != null) { + noProofNameTxData = LibSpark.createSparkNameScript( + sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, + name: txData.sparkNameInfo!.name, + additionalInfo: txData.sparkNameInfo!.additionalInfo, + scalarHex: extractedTx.getId(), privateKeyHex: privateKey.toHex, - index: kDefaultSparkIndex, - recipients: txData.recipients - ?.map( - (e) => ( - address: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - ), - ) - .toList() ?? - [], - privateRecipients: txData.sparkRecipients - ?.map( - (e) => ( - sparkAddress: e.address, - amount: e.amount.raw.toInt(), - subtractFeeFromAmount: isSendAll, - memo: e.memo, - ), - ) - .toList() ?? - [], - serializedCoins: serializedCoins, - allAnonymitySets: allAnonymitySets, - idAndBlockHashes: idAndBlockHashes - .map( - (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), - ) - .toList(), - txHash: extractedTx.getHash(), - ), - ); + spendKeyIndex: kDefaultSparkIndex, + diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, + isTestNet: cryptoCurrency.network != CryptoCurrencyNetwork.main, + ignoreProof: true, + hashFailSafe: 0, + ); + } + + final spend = await computeWithLibSparkLogging(_createSparkSend, ( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + recipients: + txData.recipients + ?.map( + (e) => ( + address: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + ), + ) + .toList() ?? + [], + privateRecipients: + txData.sparkRecipients + ?.map( + (e) => ( + sparkAddress: e.address, + amount: e.amount.raw.toInt(), + subtractFeeFromAmount: isSendAll, + memo: e.memo, + ), + ) + .toList() ?? + [], + serializedCoins: serializedCoins, + allAnonymitySets: allAnonymitySets, + idAndBlockHashes: + idAndBlockHashes + .map( + (e) => (setId: e.groupId, blockHash: base64Decode(e.blockHash)), + ) + .toList(), + txHash: extractedTx.getHash(), + additionalTxSize: + txData.sparkNameInfo == null ? 0 : noProofNameTxData!.size, + )); for (final outputScript in spend.outputScripts) { extractedTx.addOutput(outputScript, 0); } extractedTx.setPayload(spend.serializedSpendPayload); + + if (txData.sparkNameInfo != null) { + // this is name reg tx + + extractedTx.setPayload( + Uint8List.fromList([ + ...spend.serializedSpendPayload, + ...noProofNameTxData!.script, + ]), + ); + + final hash = extractedTx.getId(); + + ({Uint8List script, int size})? nameScriptData; + int hashFailSafe = 0; + while (nameScriptData == null) { + try { + nameScriptData = LibSpark.createSparkNameScript( + sparkNameValidityBlocks: txData.sparkNameInfo!.validBlocks, + name: txData.sparkNameInfo!.name, + additionalInfo: txData.sparkNameInfo!.additionalInfo, + scalarHex: hash, + privateKeyHex: privateKey.toHex, + spendKeyIndex: kDefaultSparkIndex, + diversifier: txData.sparkNameInfo!.sparkAddress.derivationIndex, + isTestNet: cryptoCurrency.network != CryptoCurrencyNetwork.main, + ignoreProof: false, + hashFailSafe: hashFailSafe, + ); + break; + } catch (e) { + if (e.toString() != "Exception: hash fail") { + rethrow; + } + hashFailSafe++; + } + } + + extractedTx.setPayload( + Uint8List.fromList([ + ...spend.serializedSpendPayload, + ...nameScriptData.script, + ]), + ); + } + final rawTxHex = extractedTx.toHex(); if (isSendAll) { @@ -687,10 +731,11 @@ mixin SparkInterface sequence: 0xffffffff, outpoint: null, addresses: [], - valueStringSats: tempOutputs - .map((e) => e.value) - .fold(fee.raw, (p, e) => p + e) - .toString(), + valueStringSats: + tempOutputs + .map((e) => e.value) + .fold(fee.raw, (p, e) => p + e) + .toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -709,12 +754,10 @@ mixin SparkInterface usedCoin.height == e.height && usedCoin.groupId == e.groupId && base64Decode(e.serializedCoinB64!).toHex.startsWith( - base64Decode(usedCoin.serializedCoin).toHex, - ), + base64Decode(usedCoin.serializedCoin).toHex, + ), ) - .copyWith( - isUsed: true, - ), + .copyWith(isUsed: true), ); } catch (_) { throw Exception( @@ -735,15 +778,12 @@ mixin SparkInterface timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(tempInputs), outputs: List.unmodifiable(tempOutputs), - type: tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.sparkSpend, - otherData: jsonEncode( - { - "overrideFee": fee.toJsonString(), - }, - ), + otherData: jsonEncode({"overrideFee": fee.toJsonString()}), height: null, version: 3, ), @@ -752,9 +792,7 @@ mixin SparkInterface } // this may not be needed for either mints or spends or both - Future confirmSendSpark({ - required TxData txData, - }) async { + Future confirmSendSpark({required TxData txData}) async { try { Logging.instance.d("confirmSend txData: $txData"); @@ -821,11 +859,7 @@ mixin SparkInterface for (final data in sparkDataToCheck) { for (int i = 0; i < data.coins.length; i++) { - rawCoins.add([ - data.coins[i], - data.txid, - data.serialContext.first, - ]); + rawCoins.add([data.coins[i], data.txid, data.serialContext.first]); } checkedTxids.add(data.txid); @@ -836,16 +870,13 @@ mixin SparkInterface // if there is new data we try and identify the coins if (rawCoins.isNotEmpty) { // run identify off main isolate - final myCoins = await computeWithLibSparkLogging( - _identifyCoins, - ( - anonymitySetCoins: rawCoins, - groupId: groupId, - privateKeyHexSet: privateKeyHexSet, - walletId: walletId, - isTestNet: cryptoCurrency.network.isTestNet, - ), - ); + final myCoins = await computeWithLibSparkLogging(_identifyCoins, ( + anonymitySetCoins: rawCoins, + groupId: groupId, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network.isTestNet, + )); // add checked txids after identification _mempoolTxidsChecked.addAll(checkedTxids); @@ -872,21 +903,13 @@ mixin SparkInterface // returns next percent double _triggerEventHelper(double current, double increment) { refreshingPercent = current; - GlobalEventBus.instance.fire( - RefreshPercentChangedEvent( - current, - walletId, - ), - ); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(current, walletId)); return current + increment; } // Linearly make calls so there is less chance of timing out or otherwise breaking Future refreshSparkData( - ( - double startingPercent, - double endingPercent, - )? refreshProgressRange, + (double startingPercent, double endingPercent)? refreshProgressRange, ) async { final start = DateTime.now(); try { @@ -898,9 +921,9 @@ mixin SparkInterface for (int id = 1; id < latestGroupId; id++) { final setExists = await FiroCacheCoordinator.checkSetInfoForGroupIdExists( - id, - cryptoCurrency.network, - ); + id, + cryptoCurrency.network, + ); if (!setExists) { groupIds.add(id); } @@ -908,7 +931,8 @@ mixin SparkInterface } groupIds.add(latestGroupId); - final steps = groupIds.length + + final steps = + groupIds.length + 1 // get used tags step + 1 // check updated cache step @@ -919,9 +943,10 @@ mixin SparkInterface + 1; // update balance - final percentIncrement = refreshProgressRange == null - ? null - : (refreshProgressRange.$2 - refreshProgressRange.$1) / steps; + final percentIncrement = + refreshProgressRange == null + ? null + : (refreshProgressRange.$2 - refreshProgressRange.$1) / steps; double currentPercent = refreshProgressRange?.$1 ?? 0; // fetch and update process for each set groupId as required @@ -963,8 +988,8 @@ mixin SparkInterface // after that block. final groupIdBlockHashMap = info.otherData[WalletInfoKeys.firoSparkCacheSetBlockHashCache] - as Map? ?? - {}; + as Map? ?? + {}; // iterate through the cache, fetching spark coin data that hasn't been // processed by this wallet yet @@ -977,19 +1002,14 @@ mixin SparkInterface ); final anonymitySetResult = await FiroCacheCoordinator.getSetCoinsForGroupId( - i, - afterBlockHash: lastCheckedHash, - network: cryptoCurrency.network, - ); - final coinsRaw = anonymitySetResult - .map( - (e) => [ - e.serialized, - e.txHash, - e.context, - ], - ) - .toList(); + i, + afterBlockHash: lastCheckedHash, + network: cryptoCurrency.network, + ); + final coinsRaw = + anonymitySetResult + .map((e) => [e.serialized, e.txHash, e.context]) + .toList(); if (coinsRaw.isNotEmpty) { rawCoinsBySetId[i] = coinsRaw; @@ -1005,33 +1025,36 @@ mixin SparkInterface // get address(es) to get the private key hex strings required for // identifying spark coins - final sparkAddresses = await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .typeEqualTo(AddressType.spark) - .findAll(); + final sparkAddresses = + await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .findAll(); final root = await getRootHDNode(); - final Set privateKeyHexSet = sparkAddresses - .map( - (e) => - root.derivePath(e.derivationPath!.value).privateKey.data.toHex, - ) - .toSet(); + final Set privateKeyHexSet = + sparkAddresses + .map( + (e) => + root + .derivePath(e.derivationPath!.value) + .privateKey + .data + .toHex, + ) + .toSet(); // try to identify any coins in the unchecked set data final List newlyIdCoins = []; for (final groupId in rawCoinsBySetId.keys) { - final myCoins = await computeWithLibSparkLogging( - _identifyCoins, - ( - anonymitySetCoins: rawCoinsBySetId[groupId]!, - groupId: groupId, - privateKeyHexSet: privateKeyHexSet, - walletId: walletId, - isTestNet: cryptoCurrency.network.isTestNet, - ), - ); + final myCoins = await computeWithLibSparkLogging(_identifyCoins, ( + anonymitySetCoins: rawCoinsBySetId[groupId]!, + groupId: groupId, + privateKeyHexSet: privateKeyHexSet, + walletId: walletId, + isTestNet: cryptoCurrency.network.isTestNet, + )); newlyIdCoins.addAll(myCoins); } // if any were found, add to database @@ -1066,14 +1089,15 @@ mixin SparkInterface } // get unused and or unconfirmed coins from db - final coinsToCheck = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .heightIsNull() - .or() - .isUsedEqualTo(false) - .findAll(); + final coinsToCheck = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .heightIsNull() + .or() + .isUsedEqualTo(false) + .findAll(); Set? spentCoinTags; // only fetch tags from db if we need them to compare against any items @@ -1104,9 +1128,10 @@ mixin SparkInterface checked = coin; } } else { - checked = spentCoinTags!.contains(coin.lTagHash) - ? coin.copyWith(isUsed: true) - : coin; + checked = + spentCoinTags!.contains(coin.lTagHash) + ? coin.copyWith(isUsed: true) + : coin; } checkedCoins.add(checked); @@ -1126,12 +1151,15 @@ mixin SparkInterface final currentHeight = await chainHeight; // get all unused coins to update wallet spark balance - final unusedCoins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(false) - .findAll(); + final unusedCoins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .findAll(); + + final sparkNamesUpdateFuture = refreshSparkNames(); final total = Amount( rawValue: unusedCoins @@ -1164,9 +1192,14 @@ mixin SparkInterface newBalance: sparkBalance, isar: mainDB.isar, ); + + await sparkNamesUpdateFuture; } catch (e, s) { - Logging.instance - .e("$runtimeType $walletId ${info.name}: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); rethrow; } finally { Logging.instance.d( @@ -1177,13 +1210,14 @@ mixin SparkInterface } Future> getSparkSpendTransactionIds() async { - final tags = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .isUsedEqualTo(true) - .lTagHashProperty() - .findAll(); + final tags = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(true) + .lTagHashProperty() + .findAll(); final pairs = await FiroCacheCoordinator.getUsedCoinTxidsFor( tags: tags, @@ -1195,9 +1229,7 @@ mixin SparkInterface /// Should only be called within the standard wallet [recover] function due to /// mutex locking. Otherwise behaviour MAY be undefined. - Future recoverSparkWallet({ - required int latestSparkCoinId, - }) async { + Future recoverSparkWallet({required int latestSparkCoinId}) async { // generate spark addresses if non existing if (await getCurrentReceivingSparkAddress() == null) { final address = await generateNextSparkAddress(); @@ -1207,12 +1239,124 @@ mixin SparkInterface try { await refreshSparkData(null); } catch (e, s) { - Logging.instance - .e("$runtimeType $walletId ${info.name}: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); rethrow; } } + Future refreshSparkNames() async { + try { + Logging.instance.i("Refreshing spark names for $walletId ${info.name}"); + + final db = Drift.get(walletId); + final myNameStrings = + await db.managers.sparkNames.map((e) => e.name).get(); + final names = await electrumXClient.getSparkNames(); + + // start update shared cache of all names + final nameUpdateFuture = SparkNamesService.update( + names, + network: cryptoCurrency.network, + ); + + final myAddresses = + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.spark) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .valueProperty() + .findAll()) + .toSet(); + + // some look ahead + // TODO revisit this and clean up (track pre gen'd addresses instead of generating every time) + // arbitrary number of addresses + const lookAheadCount = 100; + final highestStoredDiversifier = + (await getCurrentReceivingSparkAddress())?.derivationIndex; + + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network.isTestNet) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final keys = root.derivePath(derivationPath); + + // default to starting at 1 if none found + int diversifier = (highestStoredDiversifier ?? 0) + 1; + + final maxDiversifier = diversifier + lookAheadCount; + + while (diversifier < maxDiversifier) { + // change address check + if (diversifier == kSparkChange) { + diversifier++; + } + final addressString = await LibSpark.getAddress( + privateKey: keys.privateKey.data, + index: kDefaultSparkIndex, + diversifier: diversifier, + isTestNet: cryptoCurrency.network.isTestNet, + ); + + myAddresses.add(addressString); + + diversifier++; + } + + names.retainWhere( + (e) => + myAddresses.contains(e.address) && !myNameStrings.contains(e.name), + ); + Logging.instance.d("Found $names new spark names"); + + if (names.isNotEmpty) { + final List< + ({ + String name, + String address, + int validUntil, + String? additionalInfo, + }) + > + data = []; + + for (final name in names) { + final info = await electrumXClient.getSparkNameData( + sparkName: name.name, + ); + + data.add(( + name: name.name, + address: name.address, + validUntil: info.validUntil, + additionalInfo: info.additionalInfo, + )); + } + + await db.upsertSparkNames(data); + } + + // finally ensure shared cache update has completed + await nameUpdateFuture; + } catch (e, s) { + Logging.instance.e( + "refreshing spark names for $walletId \"${info.name}\" failed", + error: e, + stackTrace: s, + ); + } + } + // modelled on CSparkWallet::CreateSparkMintTransactions https://github.com/firoorg/firo/blob/39c41e5e7ec634ced3700fe3f4f5509dc2e480d0/src/spark/sparkwallet.cpp#L752 Future> _createSparkMintTransactions({ required List availableUtxos, @@ -1229,8 +1373,9 @@ mixin SparkInterface // addresses when confirming the transactions later as well assert(outputs.length == 1); - BigInt valueToMint = - outputs.map((e) => e.value).reduce((value, element) => value + element); + BigInt valueToMint = outputs + .map((e) => e.value) + .reduce((value, element) => value + element); if (valueToMint <= BigInt.zero) { throw Exception("Cannot mint amount=$valueToMint"); @@ -1251,9 +1396,10 @@ mixin SparkInterface // setup some vars int nChangePosInOut = -1; final int nChangePosRequest = nChangePosInOut; - List outputs_ = outputs - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); // deep copy + List outputs_ = + outputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); // deep copy final feesObject = await fees; final currentHeight = await chainHeight; final random = Random.secure(); @@ -1262,9 +1408,10 @@ mixin SparkInterface valueAndUTXOs.shuffle(random); while (valueAndUTXOs.isNotEmpty) { - final lockTime = random.nextInt(10) == 0 - ? max(0, currentHeight - random.nextInt(100)) - : currentHeight; + final lockTime = + random.nextInt(10) == 0 + ? max(0, currentHeight - random.nextInt(100)) + : currentHeight; const txVersion = 1; final List vin = []; final List<(dynamic, int, String?)> vout = []; @@ -1311,9 +1458,10 @@ mixin SparkInterface setCoins.clear(); // deep copy - final remainingOutputs = outputs_ - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); + final remainingOutputs = + outputs_ + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); final List singleTxOutputs = []; if (autoMintAll) { @@ -1328,8 +1476,10 @@ mixin SparkInterface BigInt remainingMintValue = BigInt.parse(mintedValue.toString()); while (remainingMintValue > BigInt.zero) { - final singleMintValue = - _min(remainingMintValue, remainingOutputs.first.value); + final singleMintValue = _min( + remainingMintValue, + remainingOutputs.first.value, + ); singleTxOutputs.add( MutableSparkRecipient( remainingOutputs.first.address, @@ -1372,15 +1522,16 @@ mixin SparkInterface // Generate dummy mint coins to save time final dummyRecipients = LibSpark.createSparkMintRecipients( - outputs: singleTxOutputs - .map( - (e) => ( - sparkAddress: e.address, - value: e.value.toInt(), - memo: "", - ), - ) - .toList(), + outputs: + singleTxOutputs + .map( + (e) => ( + sparkAddress: e.address, + value: e.value.toInt(), + memo: "", + ), + ) + .toList(), serialContext: Uint8List(0), generate: false, ); @@ -1393,13 +1544,11 @@ mixin SparkInterface if (recipient.amount < cryptoCurrency.dustLimit.raw.toInt()) { throw Exception("Output amount too small"); } - vout.add( - ( - recipient.scriptPubKey, - recipient.amount, - singleTxOutputs[i].address, - ), - ); + vout.add(( + recipient.scriptPubKey, + recipient.amount, + singleTxOutputs[i].address, + )); } // Choose coins to use @@ -1429,10 +1578,11 @@ mixin SparkInterface } final changeAddress = await getCurrentChangeAddress(); - vout.insert( - nChangePosInOut, - (changeAddress!.value, nChange.toInt(), null), - ); + vout.insert(nChangePosInOut, ( + changeAddress!.value, + nChange.toInt(), + null, + )); } } @@ -1450,42 +1600,40 @@ mixin SparkInterface switch (sd.derivePathType) { case DerivePathType.bip44: - data = btc - .P2PKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + data = + btc + .P2PKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip49: - final p2wpkh = btc - .P2WPKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; - data = btc - .P2SH( - data: btc.PaymentData(redeem: p2wpkh), - network: _bitcoinDartNetwork, - ) - .data; + final p2wpkh = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; + data = + btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip84: - data = btc - .P2WPKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + data = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -1530,10 +1678,7 @@ mixin SparkInterface } final nFeeNeeded = BigInt.from( - estimateTxFee( - vSize: nBytes, - feeRatePerKB: feesObject.medium, - ), + estimateTxFee(vSize: nBytes, feeRatePerKB: feesObject.medium), ); // One day we'll do this properly if (nFeeRet >= nFeeNeeded) { @@ -1548,25 +1693,19 @@ mixin SparkInterface // Generate real mint coins final serialContext = LibSpark.serializeMintContext( - inputs: setCoins - .map( - (e) => ( - e.utxo.txid, - e.utxo.vout, - ), - ) - .toList(), + inputs: setCoins.map((e) => (e.utxo.txid, e.utxo.vout)).toList(), ); final recipients = LibSpark.createSparkMintRecipients( - outputs: singleTxOutputs - .map( - (e) => ( - sparkAddress: e.address, - memo: e.memo, - value: e.value.toInt(), - ), - ) - .toList(), + outputs: + singleTxOutputs + .map( + (e) => ( + sparkAddress: e.address, + memo: e.memo, + value: e.value.toInt(), + ), + ) + .toList(), serialContext: serialContext, generate: true, ); @@ -1591,9 +1730,10 @@ mixin SparkInterface } // deep copy - outputs_ = remainingOutputs - .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) - .toList(); + outputs_ = + remainingOutputs + .map((e) => MutableSparkRecipient(e.address, e.value, e.memo)) + .toList(); break; // Done, enough fee included. } @@ -1621,42 +1761,40 @@ mixin SparkInterface switch (input.derivePathType) { case DerivePathType.bip44: - data = btc - .P2PKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + data = + btc + .P2PKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip49: - final p2wpkh = btc - .P2WPKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; - data = btc - .P2SH( - data: btc.PaymentData(redeem: p2wpkh), - network: _bitcoinDartNetwork, - ) - .data; + final p2wpkh = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; + data = + btc + .P2SH( + data: btc.PaymentData(redeem: p2wpkh), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip84: - data = btc - .P2WPKH( - data: btc.PaymentData( - pubkey: pubKey, - ), - network: _bitcoinDartNetwork, - ) - .data; + data = + btc + .P2WPKH( + data: btc.PaymentData(pubkey: pubKey), + network: _bitcoinDartNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -1707,7 +1845,8 @@ mixin SparkInterface addresses: [ if (addressOrScript is String) addressOrScript.toString(), ], - walletOwns: (await mainDB.isar.addresses + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -1752,21 +1891,24 @@ mixin SparkInterface assert(outputs.length == 1); final data = TxData( - sparkRecipients: vout - .where((e) => e.$1 is Uint8List) // ignore change - .map( - (e) => ( - address: outputs.first - .address, // for display purposes on confirm tx screen. See todos above - memo: "", - amount: Amount( - rawValue: BigInt.from(e.$2), - fractionDigits: cryptoCurrency.fractionDigits, - ), - isChange: false, // ok? - ), - ) - .toList(), + sparkRecipients: + vout + .where((e) => e.$1 is Uint8List) // ignore change + .map( + (e) => ( + address: + outputs + .first + .address, // for display purposes on confirm tx screen. See todos above + memo: "", + amount: Amount( + rawValue: BigInt.from(e.$2), + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: false, // ok? + ), + ) + .toList(), vSize: builtTx.virtualSize(), txid: builtTx.getId(), raw: builtTx.toHex(), @@ -1853,23 +1995,25 @@ mixin SparkInterface const subtractFeeFromAmount = true; // must be true for mint all final currentHeight = await chainHeight; - final spendableUtxos = await mainDB.isar.utxos - .where() - .walletIdEqualTo(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group((q) => q.usedEqualTo(false).or().usedIsNull()) - .and() - .valueGreaterThan(0) - .findAll(); + final spendableUtxos = + await mainDB.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedEqualTo(false).or().usedIsNull()) + .and() + .valueGreaterThan(0) + .findAll(); spendableUtxos.removeWhere( - (e) => !e.isConfirmed( - currentHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); if (spendableUtxos.isEmpty) { @@ -1910,15 +2054,12 @@ mixin SparkInterface if (txData.sparkRecipients?.isNotEmpty != true) { throw Exception("Missing spark recipients."); } - final recipients = txData.sparkRecipients! - .map( - (e) => MutableSparkRecipient( - e.address, - e.amount.raw, - e.memo, - ), - ) - .toList(); + final recipients = + txData.sparkRecipients! + .map( + (e) => MutableSparkRecipient(e.address, e.amount.raw, e.memo), + ) + .toList(); final total = recipients .map((e) => e.value) @@ -1933,11 +2074,12 @@ mixin SparkInterface final utxos = txData.utxos; final bool coinControl = utxos != null; - final utxosTotal = coinControl - ? utxos - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)) - : null; + final utxosTotal = + coinControl + ? utxos + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + BigInt.from(e)) + : null; if (coinControl && utxosTotal! < total) { throw Exception("Insufficient selected UTXOs!"); @@ -1947,7 +2089,8 @@ mixin SparkInterface final currentHeight = await chainHeight; - final availableOutputs = utxos?.toList() ?? + final availableOutputs = + utxos?.toList() ?? await mainDB.isar.utxos .where() .walletIdEqualTo(walletId) @@ -1961,17 +2104,18 @@ mixin SparkInterface final canCPFP = this is CpfpInterface && coinControl; - final spendableUtxos = availableOutputs - .where( - (e) => - canCPFP || - e.isConfirmed( - currentHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), - ) - .toList(); + final spendableUtxos = + availableOutputs + .where( + (e) => + canCPFP || + e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), + ) + .toList(); if (spendableUtxos.isEmpty) { throw Exception("No available UTXOs found to anonymize"); @@ -2015,6 +2159,80 @@ mixin SparkInterface return txData.copyWith(sparkMints: await Future.wait(futures)); } + Future prepareSparkNameTransaction({ + required String name, + required String address, + required int years, + required String additionalInfo, + }) async { + if (years < 1 || years > kMaxNameRegistrationLengthYears) { + throw Exception("Invalid spark name registration period years: $years"); + } + + if (name.isEmpty || name.length > kMaxNameLength) { + throw Exception("Invalid spark name length: ${name.length}"); + } + if (!RegExp(kNameRegexString).hasMatch(name)) { + throw Exception("Invalid symbols found in spark name: $name"); + } + + if (additionalInfo.toUint8ListFromUtf8.length > + kMaxAdditionalInfoLengthBytes) { + throw Exception( + "Additional info exceeds $kMaxAdditionalInfoLengthBytes bytes.", + ); + } + + final sparkAddress = await mainDB.getAddress(walletId, address); + if (sparkAddress == null) { + throw Exception("Address '$address' not found in local DB."); + } + if (sparkAddress.type != AddressType.spark) { + throw Exception("Address '$address' is not a spark address."); + } + + final data = ( + name: name, + additionalInfo: additionalInfo, + validBlocks: years * 365 * 24 * 24, + sparkAddress: sparkAddress, + ); + + final String destinationAddress; + switch (cryptoCurrency.network) { + case CryptoCurrencyNetwork.main: + destinationAddress = kStage3DevelopmentFundAddressMainNet; + break; + + case CryptoCurrencyNetwork.test: + destinationAddress = kStage3DevelopmentFundAddressTestNet; + break; + + default: + throw Exception( + "Invalid network '${cryptoCurrency.network}' for spark name registration.", + ); + } + + final txData = await prepareSendSpark( + txData: TxData( + sparkNameInfo: data, + recipients: [ + ( + address: destinationAddress, + amount: Amount.fromDecimal( + Decimal.fromInt(kStandardSparkNamesFee[name.length] * years), + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: false, + ), + ], + ), + ); + + return txData; + } + @override Future updateBalance() async { // call to super to update transparent balance (and lelantus balance if @@ -2031,63 +2249,71 @@ mixin SparkInterface // ====================== Private ============================================ btc.NetworkType get _bitcoinDartNetwork => btc.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); } /// Top level function which should be called wrapped in [compute] Future< - ({ - Uint8List serializedSpendPayload, - List outputScripts, - int fee, - List< - ({ - int groupId, - int height, - String serializedCoin, - String serializedCoinContext - })> usedCoins, - })> _createSparkSend( + ({ + Uint8List serializedSpendPayload, + List outputScripts, + int fee, + List< + ({ + int groupId, + int height, + String serializedCoin, + String serializedCoinContext, + }) + > + usedCoins, + }) +> +_createSparkSend( ({ String privateKeyHex, int index, List<({String address, int amount, bool subtractFeeFromAmount})> recipients, List< - ({ - String sparkAddress, - int amount, - bool subtractFeeFromAmount, - String memo - })> privateRecipients, + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo, + }) + > + privateRecipients, List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> serializedCoins, + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + }) + > + serializedCoins, List< - ({ - int setId, - String setHash, - List<({String serializedCoin, String txHash})> set - })> allAnonymitySets, - List< - ({ - int setId, - Uint8List blockHash, - })> idAndBlockHashes, + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set, + }) + > + allAnonymitySets, + List<({int setId, Uint8List blockHash})> idAndBlockHashes, Uint8List txHash, - }) args, + int additionalTxSize, + }) + args, ) async { final spend = LibSpark.createSparkSendTransaction( privateKeyHex: args.privateKeyHex, @@ -2098,6 +2324,7 @@ Future< allAnonymitySets: args.allAnonymitySets, idAndBlockHashes: args.idAndBlockHashes, txHash: args.txHash, + additionalTxSize: args.additionalTxSize, ); return spend; @@ -2111,7 +2338,8 @@ Future> _identifyCoins( Set privateKeyHexSet, String walletId, bool isTestNet, - }) args, + }) + args, ) async { final List myCoins = []; @@ -2200,12 +2428,13 @@ class MutableSparkRecipient { } } -typedef SerializedCoinData = ({ - int groupId, - int height, - String serializedCoin, - String serializedCoinContext -}); +typedef SerializedCoinData = + ({ + int groupId, + int height, + String serializedCoin, + String serializedCoinContext, + }); Future _asyncSparkFeesWrapper({ required String privateKeyHex, @@ -2214,18 +2443,19 @@ Future _asyncSparkFeesWrapper({ required bool subtractFeeFromAmount, required List serializedCoins, required int privateRecipientsCount, + required int utxoNum, + required int additionalTxSize, }) async { - return await computeWithLibSparkLogging( - _estSparkFeeComputeFunc, - ( - privateKeyHex: privateKeyHex, - index: index, - sendAmount: sendAmount, - subtractFeeFromAmount: subtractFeeFromAmount, - serializedCoins: serializedCoins, - privateRecipientsCount: privateRecipientsCount, - ), - ); + return await computeWithLibSparkLogging(_estSparkFeeComputeFunc, ( + privateKeyHex: privateKeyHex, + index: index, + sendAmount: sendAmount, + subtractFeeFromAmount: subtractFeeFromAmount, + serializedCoins: serializedCoins, + privateRecipientsCount: privateRecipientsCount, + utxoNum: utxoNum, + additionalTxSize: additionalTxSize, + )); } int _estSparkFeeComputeFunc( @@ -2236,7 +2466,10 @@ int _estSparkFeeComputeFunc( bool subtractFeeFromAmount, List serializedCoins, int privateRecipientsCount, - }) args, + int utxoNum, + int additionalTxSize, + }) + args, ) { final est = LibSpark.estimateSparkFee( privateKeyHex: args.privateKeyHex, @@ -2245,6 +2478,8 @@ int _estSparkFeeComputeFunc( subtractFeeFromAmount: args.subtractFeeFromAmount, serializedCoins: args.serializedCoins, privateRecipientsCount: args.privateRecipientsCount, + utxoNum: args.utxoNum, + additionalTxSize: args.additionalTxSize, ); return est; diff --git a/lib/widgets/static_overflow_row/measure_size.dart b/lib/widgets/static_overflow_row/measure_size.dart new file mode 100644 index 000000000..d6d9f9708 --- /dev/null +++ b/lib/widgets/static_overflow_row/measure_size.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class MeasureSize extends StatefulWidget { + const MeasureSize({super.key, required this.onChange, required this.child}); + + final ValueChanged onChange; + final Widget child; + + @override + State createState() => _MeasureSizeState(); +} + +class _MeasureSizeState extends State { + Size? previous; + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final size = context.size; + if (size != null && previous != size) { + previous = size; + widget.onChange(size); + } + }); + return widget.child; + } +} diff --git a/lib/widgets/static_overflow_row/static_overflow_row.dart b/lib/widgets/static_overflow_row/static_overflow_row.dart new file mode 100644 index 000000000..77cad9ada --- /dev/null +++ b/lib/widgets/static_overflow_row/static_overflow_row.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; + +import 'measure_size.dart'; + +class StaticOverflowRow extends StatefulWidget { + final Widget Function(int hiddenCount) overflowBuilder; + final MainAxisAlignment mainAxisAlignment; + final List children; + final bool forcedOverflow; + + const StaticOverflowRow({ + super.key, + required this.overflowBuilder, + this.mainAxisAlignment = MainAxisAlignment.end, + this.forcedOverflow = false, + required this.children, + }); + + @override + State createState() => _StaticOverflowRowState(); +} + +class _StaticOverflowRowState extends State { + final Map _itemSizes = {}; + Size? _overflowSize; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final childCount = widget.children.length; + + // Still measuring + if (_itemSizes.length < childCount || _overflowSize == null) { + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: [ + ...List.generate(childCount, (i) { + return MeasureSize( + onChange: (size) { + if (_itemSizes[i] != size) { + setState(() { + _itemSizes[i] = size; + }); + } + }, + child: KeyedSubtree( + key: ValueKey("item-$i"), + child: widget.children[i], + ), + ); + }), + MeasureSize( + onChange: (size) { + if (_overflowSize != size) { + setState(() { + _overflowSize = size; + }); + } + }, + child: KeyedSubtree( + key: const ValueKey("overflow"), + child: widget.overflowBuilder(0), + ), + ), + ], + ); + } + + final List visible = []; + double usedWidth = (widget.forcedOverflow ? _overflowSize!.width : 0); + + bool firstPassFailed = false; + // Try first pass without overflow + for (int i = 0; i < childCount; i++) { + final itemSize = _itemSizes[i]!; + if (usedWidth + itemSize.width <= constraints.maxWidth) { + visible.add(widget.children[i]); + usedWidth += itemSize.width; + } else { + // Not all children fit. Overflow required + firstPassFailed = true; + break; + } + } + + if (firstPassFailed) { + visible.clear(); + usedWidth = 0; + int overflowCount = 0; + for (int i = 0; i < childCount; i++) { + final size = _itemSizes[i]!; + final needsOverflow = i < childCount - 1 || widget.forcedOverflow; + final canFit = + usedWidth + + size.width + + (needsOverflow ? _overflowSize!.width : 0) <= + constraints.maxWidth; + + if (canFit) { + visible.add(widget.children[i]); + usedWidth += size.width; + } else { + overflowCount = childCount - i; + break; + } + } + + // Add overflow + visible.add(widget.overflowBuilder(overflowCount)); + } else { + if (widget.forcedOverflow) { + // Add forced overflow + visible.add(widget.overflowBuilder(0)); + } + } + + return Row( + mainAxisAlignment: widget.mainAxisAlignment, + children: visible, + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 9d65621e5..abb45eb85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.11.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" another_flushbar: dependency: "direct main" description: @@ -665,6 +673,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: c2d073d35ad441730812f4ea05b5dd031fb81c5f9786a4f5fb77ecd6307b6f74 + url: "https://pub.dev" + source: hosted + version: "2.22.1" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: f4ab5d6976b1e31551ceb82ff597a505bda7818ff4f7be08a1da9d55eb6e730c + url: "https://pub.dev" + source: hosted + version: "2.22.1" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: "9fd9b479c6187d6b3bbdfd2703df98010470a6c65c2a8c8c5a1034c620bd0a0e" + url: "https://pub.dev" + source: hosted + version: "0.2.3" dropdown_button2: dependency: "direct main" description: @@ -832,11 +864,11 @@ packages: dependency: "direct main" description: path: "." - ref: e8c502652da7836cd1a22893339838553675b464 - resolved-ref: e8c502652da7836cd1a22893339838553675b464 + ref: "66dab036ed7332fd4b0609e5eb59b0c1ac3eb98b" + resolved-ref: "66dab036ed7332fd4b0609e5eb59b0c1ac3eb98b" url: "https://github.com/cypherstack/flutter_libsparkmobile.git" source: git - version: "0.0.2" + version: "0.1.0" flutter_lints: dependency: "direct dev" description: @@ -1475,7 +1507,7 @@ packages: source: hosted version: "3.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" @@ -1706,6 +1738,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" retry: dependency: transitive description: @@ -1861,18 +1901,26 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: b384f598b813b347c5a7e5ffad82cbaff1bec3d1561af267041e66f6f0899295 + sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.5" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "1e62698dc1ab396152ccaf3b3990d826244e9f3c8c39b51805f209adcd6dbea3" + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + url: "https://pub.dev" + source: hosted + version: "0.5.24" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "4cad4b2c5f63dc9ea1a8dcffb58cf762322bea5dd8836870164a65e913bdae41" url: "https://pub.dev" source: hosted - version: "0.5.22" + version: "0.40.0" stack_trace: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 86b62194d..3019844ad 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -38,7 +38,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: e8c502652da7836cd1a22893339838553675b464 + ref: 66dab036ed7332fd4b0609e5eb59b0c1ac3eb98b # cs_monero compat (unpublished) compat: @@ -188,8 +188,8 @@ dependencies: ref: dea799c20bc917f72b18c916ca96bc99fb1bd1c5 path: packages/solana calendar_date_picker2: ^1.0.2 - sqlite3: 2.4.3 - sqlite3_flutter_libs: 0.5.22 + sqlite3: 2.4.5 + sqlite3_flutter_libs: 0.5.24 # camera_linux: ^0.0.8 camera_linux: git: @@ -218,6 +218,9 @@ dependencies: git: url: https://github.com/Cyrix126/namecoin_dart ref: 819b21164ef93cc0889049d4a8a1be2d0cc36a1b + drift: ^2.22.1 + drift_flutter: ^0.2.3 + path: ^1.9.1 dev_dependencies: flutter_test: @@ -238,6 +241,7 @@ dev_dependencies: isar_generator: version: 3.1.8 hosted: https://pub.isar-community.dev/ + drift_dev: ^2.22.1 flutter_native_splash: image: assets/icon/splash.png diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 4ac0b9849..11c558f20 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -531,6 +531,67 @@ class MockElectrumXClient extends _i1.Mock implements _i6.ElectrumXClient { returnValue: _i9.Future>>.value(>[]), ) as _i9.Future>>); + @override + _i9.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i9.Future>.value( + <({String address, String name})>[]), + ) as _i9.Future>); + + @override + _i9.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i9.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i8.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i8.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i9.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i9.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index a36aefef0..c840f4b50 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index c853a3f37..50706b9cc 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 08f884b57..93185c901 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index cd27b6654..287146e12 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID, diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 1360bd6db..8a1cce451 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -527,6 +527,67 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i8.Future>>.value(>[]), ) as _i8.Future>>); + @override + _i8.Future> getSparkNames( + {String? requestID}) => + (super.noSuchMethod( + Invocation.method( + #getSparkNames, + [], + {#requestID: requestID}, + ), + returnValue: _i8.Future>.value( + <({String address, String name})>[]), + ) as _i8.Future>); + + @override + _i8.Future<({String additionalInfo, String address, int validUntil})> + getSparkNameData({ + required String? sparkName, + String? requestID, + }) => + (super.noSuchMethod( + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + returnValue: _i8.Future< + ({ + String additionalInfo, + String address, + int validUntil + })>.value(( + additionalInfo: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + address: _i7.dummyValue( + this, + Invocation.method( + #getSparkNameData, + [], + { + #sparkName: sparkName, + #requestID: requestID, + }, + ), + ), + validUntil: 0 + )), + ) as _i8.Future< + ({String additionalInfo, String address, int validUntil})>); + @override _i8.Future<_i3.SparkAnonymitySetMeta> getSparkAnonymitySetMeta({ String? requestID,