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/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 7b325030b..081ae89d8 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 7b325030bce46a423aa46497d1a608b7a8a58976 +Subproject commit 081ae89d8f47f5575a19dc03663727cf0d271f2c 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/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart index 51a9a7449..0a63c6106 100644 --- a/lib/dto/ethereum/eth_token_tx_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -28,23 +28,32 @@ class EthTokenTxDto { required this.articulatedLog, required this.transactionHash, required this.transactionIndex, + required this.blockHash, + required this.timestamp, + required this.nonce, + required this.gasUsed, + required this.gasPrice, }); EthTokenTxDto.fromMap(Map map) - : address = map['address'] as String, - blockNumber = map['blockNumber'] as int, - logIndex = map['logIndex'] as int, - topics = List.from(map['topics'] as List), - data = map['data'] as String, - articulatedLog = map['articulatedLog'] == null - ? null - : ArticulatedLog.fromMap( - Map.from( - map['articulatedLog'] as Map, - ), + : address = map['address'] as String, + blockNumber = map['blockNumber'] as int, + logIndex = map['logIndex'] as int, + topics = List.from(map['topics'] as List), + data = map['data'] as String, + articulatedLog = + map['articulatedLog'] == null + ? null + : ArticulatedLog.fromMap( + Map.from(map['articulatedLog'] as Map), ), - transactionHash = map['transactionHash'] as String, - transactionIndex = map['transactionIndex'] as int; + transactionHash = map['transactionHash'] as String, + transactionIndex = map['transactionIndex'] as int, + blockHash = map['blockHash'] as String?, + timestamp = map['timestamp'] as int, + nonce = map['nonce'] as int?, + gasUsed = map['gasUsed'] as int?, + gasPrice = BigInt.tryParse(map['gasPrice'].toString()); final String address; final int blockNumber; @@ -54,6 +63,11 @@ class EthTokenTxDto { final ArticulatedLog? articulatedLog; final String transactionHash; final int transactionIndex; + final String? blockHash; + final int timestamp; + final int? nonce; + final int? gasUsed; + final BigInt? gasPrice; EthTokenTxDto copyWith({ String? address, @@ -65,17 +79,26 @@ class EthTokenTxDto { String? compressedLog, String? transactionHash, int? transactionIndex, - }) => - EthTokenTxDto( - address: address ?? this.address, - blockNumber: blockNumber ?? this.blockNumber, - logIndex: logIndex ?? this.logIndex, - topics: topics ?? this.topics, - data: data ?? this.data, - articulatedLog: articulatedLog ?? this.articulatedLog, - transactionHash: transactionHash ?? this.transactionHash, - transactionIndex: transactionIndex ?? this.transactionIndex, - ); + String? blockHash, + int? timestamp, + int? nonce, + int? gasUsed, + BigInt? gasPrice, + }) => EthTokenTxDto( + address: address ?? this.address, + blockNumber: blockNumber ?? this.blockNumber, + logIndex: logIndex ?? this.logIndex, + topics: topics ?? this.topics, + data: data ?? this.data, + articulatedLog: articulatedLog ?? this.articulatedLog, + transactionHash: transactionHash ?? this.transactionHash, + transactionIndex: transactionIndex ?? this.transactionIndex, + blockHash: blockHash ?? this.blockHash, + timestamp: timestamp ?? this.timestamp, + nonce: nonce ?? this.nonce, + gasUsed: gasUsed ?? this.gasUsed, + gasPrice: gasPrice ?? this.gasPrice, + ); Map toMap() { final map = {}; @@ -87,6 +110,11 @@ class EthTokenTxDto { map['articulatedLog'] = articulatedLog?.toMap(); map['transactionHash'] = transactionHash; map['transactionIndex'] = transactionIndex; + map['blockHash'] = blockHash; + map['timestamp'] = timestamp; + map['nonce'] = nonce; + map['gasPrice'] = gasPrice; + map['gasUsed'] = gasUsed; return map; } @@ -100,30 +128,17 @@ class EthTokenTxDto { /// inputs : {"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"} class ArticulatedLog { - ArticulatedLog({ - required this.name, - required this.inputs, - }); + ArticulatedLog({required this.name, required this.inputs}); ArticulatedLog.fromMap(Map map) - : name = map['name'] as String, - inputs = Inputs.fromMap( - Map.from( - map['inputs'] as Map, - ), - ); + : name = map['name'] as String, + inputs = Inputs.fromMap(Map.from(map['inputs'] as Map)); final String name; final Inputs inputs; - ArticulatedLog copyWith({ - String? name, - Inputs? inputs, - }) => - ArticulatedLog( - name: name ?? this.name, - inputs: inputs ?? this.inputs, - ); + ArticulatedLog copyWith({String? name, Inputs? inputs}) => + ArticulatedLog(name: name ?? this.name, inputs: inputs ?? this.inputs); Map toMap() { final map = {}; @@ -138,31 +153,22 @@ class ArticulatedLog { /// _to : "0xc5e81fc2401b8104966637d5334cbce92f01dbf7" /// class Inputs { - Inputs({ - required this.amount, - required this.from, - required this.to, - }); + Inputs({required this.amount, required this.from, required this.to}); Inputs.fromMap(Map map) - : amount = map['_amount'] as String, - from = map['_from'] as String, - to = map['_to'] as String; + : amount = map['_amount'] as String, + from = map['_from'] as String, + to = map['_to'] as String; final String amount; final String from; final String to; - Inputs copyWith({ - String? amount, - String? from, - String? to, - }) => - Inputs( - amount: amount ?? this.amount, - from: from ?? this.from, - to: to ?? this.to, - ); + Inputs copyWith({String? amount, String? from, String? to}) => Inputs( + amount: amount ?? this.amount, + from: from ?? this.from, + to: to ?? this.to, + ); Map toMap() { final map = {}; diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart deleted file mode 100644 index 401ea7122..000000000 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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:convert'; - -import '../../utilities/amount/amount.dart'; -import '../../wallets/crypto_currency/crypto_currency.dart'; - -class EthTokenTxExtraDTO { - EthTokenTxExtraDTO({ - required this.blockHash, - required this.blockNumber, - required this.from, - required this.gas, - required this.gasCost, - required this.gasPrice, - required this.gasUsed, - required this.hash, - required this.input, - required this.nonce, - required this.timestamp, - required this.to, - required this.transactionIndex, - required this.value, - }); - - factory EthTokenTxExtraDTO.fromMap(Map map) => - EthTokenTxExtraDTO( - hash: map['hash'] as String, - blockHash: map['blockHash'] as String, - blockNumber: map['blockNumber'] as int, - transactionIndex: map['transactionIndex'] as int, - timestamp: map['timestamp'] as int, - from: map['from'] as String, - to: map['to'] as String, - value: Amount( - rawValue: BigInt.parse(map['value'] as String), - fractionDigits: Ethereum(CryptoCurrencyNetwork.main).fractionDigits, - ), - gas: _amountFromJsonNum(map['gas']), - gasPrice: _amountFromJsonNum(map['gasPrice']), - nonce: map['nonce'] as int?, - input: map['input'] as String, - gasCost: _amountFromJsonNum(map['gasCost']), - gasUsed: _amountFromJsonNum(map['gasUsed']), - ); - - final String hash; - final String blockHash; - final int blockNumber; - final int transactionIndex; - final int timestamp; - final String from; - final String to; - final Amount value; - final Amount gas; - final Amount gasPrice; - final String input; - final int? nonce; - final Amount gasCost; - final Amount gasUsed; - - static Amount _amountFromJsonNum(dynamic json) { - return Amount( - rawValue: BigInt.from(json as num), - fractionDigits: Ethereum(CryptoCurrencyNetwork.main).fractionDigits, - ); - } - - EthTokenTxExtraDTO copyWith({ - String? hash, - String? blockHash, - int? blockNumber, - int? transactionIndex, - int? timestamp, - String? from, - String? to, - Amount? value, - Amount? gas, - Amount? gasPrice, - int? nonce, - String? input, - Amount? gasCost, - Amount? gasUsed, - }) => - EthTokenTxExtraDTO( - hash: hash ?? this.hash, - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - transactionIndex: transactionIndex ?? this.transactionIndex, - timestamp: timestamp ?? this.timestamp, - from: from ?? this.from, - to: to ?? this.to, - value: value ?? this.value, - gas: gas ?? this.gas, - gasPrice: gasPrice ?? this.gasPrice, - nonce: nonce ?? this.nonce, - input: input ?? this.input, - gasCost: gasCost ?? this.gasCost, - gasUsed: gasUsed ?? this.gasUsed, - ); - - Map toMap() { - final map = {}; - map['hash'] = hash; - map['blockHash'] = blockHash; - map['blockNumber'] = blockNumber; - map['transactionIndex'] = transactionIndex; - map['timestamp'] = timestamp; - map['from'] = from; - map['to'] = to; - map['value'] = value.toJsonString(); - map['gas'] = gas.toJsonString(); - map['gasPrice'] = gasPrice.toJsonString(); - map['input'] = input; - map['nonce'] = nonce; - map['gasCost'] = gasCost.toJsonString(); - map['gasUsed'] = gasUsed.toJsonString(); - return map; - } - - @override - String toString() => jsonEncode(toMap()); -} diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index 10a46d740..0801d7050 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -31,26 +31,28 @@ class EthTxDTO { required this.hasToken, required this.gasCost, required this.gasUsed, + required this.nonce, }); factory EthTxDTO.fromMap(Map map) => EthTxDTO( - hash: map['hash'] as String, - blockHash: map['blockHash'] as String, - blockNumber: map['blockNumber'] as int, - transactionIndex: map['transactionIndex'] as int, - timestamp: map['timestamp'] as int, - from: map['from'] as String, - to: map['to'] as String, - value: _amountFromJsonNum(map['value'])!, - gas: _amountFromJsonNum(map['gas'])!, - gasPrice: _amountFromJsonNum(map['gasPrice'])!, - maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), - maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), - isError: map['isError'] as bool? ?? false, - hasToken: map['hasToken'] as bool? ?? false, - gasCost: _amountFromJsonNum(map['gasCost'])!, - gasUsed: _amountFromJsonNum(map['gasUsed'])!, - ); + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: _amountFromJsonNum(map['value'])!, + gas: _amountFromJsonNum(map['gas'])!, + gasPrice: _amountFromJsonNum(map['gasPrice'])!, + maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), + maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), + isError: map['isError'] as bool? ?? false, + hasToken: map['hasToken'] as bool? ?? false, + gasCost: _amountFromJsonNum(map['gasCost'])!, + gasUsed: _amountFromJsonNum(map['gasUsed'])!, + nonce: map['nonce'] as int?, + ); final String hash; final String blockHash; @@ -68,6 +70,7 @@ class EthTxDTO { final bool hasToken; final Amount gasCost; final Amount gasUsed; + final int? nonce; static Amount? _amountFromJsonNum(dynamic json) { if (json == null) { @@ -97,25 +100,26 @@ class EthTxDTO { String? compressedTx, Amount? gasCost, Amount? gasUsed, - }) => - EthTxDTO( - hash: hash ?? this.hash, - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - transactionIndex: transactionIndex ?? this.transactionIndex, - timestamp: timestamp ?? this.timestamp, - from: from ?? this.from, - to: to ?? this.to, - value: value ?? this.value, - gas: gas ?? this.gas, - gasPrice: gasPrice ?? this.gasPrice, - maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, - isError: isError ?? this.isError, - hasToken: hasToken ?? this.hasToken, - gasCost: gasCost ?? this.gasCost, - gasUsed: gasUsed ?? this.gasUsed, - ); + int? nonce, + }) => EthTxDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + isError: isError ?? this.isError, + hasToken: hasToken ?? this.hasToken, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + nonce: nonce ?? this.nonce, + ); Map toMap() { final map = {}; @@ -135,6 +139,7 @@ class EthTxDTO { map['hasToken'] = hasToken; map['gasCost'] = gasCost.toString(); map['gasUsed'] = gasUsed.toString(); + map['nonce'] = nonce; return map; } diff --git a/lib/dto/ethereum/pending_eth_tx_dto.dart b/lib/dto/ethereum/pending_eth_tx_dto.dart deleted file mode 100644 index a19a95f61..000000000 --- a/lib/dto/ethereum/pending_eth_tx_dto.dart +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 - * - */ - -/// blockHash : null -/// blockNumber : null -/// from : "0x..." -/// gas : "0x7e562" -/// maxPriorityFeePerGas : "0x444380" -/// maxFeePerGas : "0x342570c00" -/// hash : "0x...da64e4" -/// input : "....." -/// nonce : "0x70" -/// to : "0x00....." -/// transactionIndex : null -/// value : "0x0" -/// type : "0x2" -/// accessList : [] -/// chainId : "0x1" -/// v : "0x0" -/// r : "0xd..." -/// s : "0x17d...6e6" - -class PendingEthTxDto { - PendingEthTxDto({ - required this.blockHash, - required this.blockNumber, - required this.from, - required this.gas, - required this.maxPriorityFeePerGas, - required this.maxFeePerGas, - required this.hash, - required this.input, - required this.nonce, - required this.to, - required this.transactionIndex, - required this.value, - required this.type, - required this.accessList, - required this.chainId, - required this.v, - required this.r, - required this.s, - }); - - factory PendingEthTxDto.fromMap(Map map) => PendingEthTxDto( - blockHash: map['blockHash'] as String?, - blockNumber: map['blockNumber'] as int?, - from: map['from'] as String, - gas: map['gas'] as String, - maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as String, - maxFeePerGas: map['maxFeePerGas'] as String, - hash: map['hash'] as String, - input: map['input'] as String, - nonce: map['nonce'] as String, - to: map['to'] as String, - transactionIndex: map['transactionIndex'] as int?, - value: map['value'] as String, - type: map['type'] as String, - accessList: map['accessList'] as List? ?? [], - chainId: map['chainId'] as String, - v: map['v'] as String, - r: map['r'] as String, - s: map['s'] as String, - ); - - final String? blockHash; - final int? blockNumber; - final String from; - final String gas; - final String maxPriorityFeePerGas; - final String maxFeePerGas; - final String hash; - final String input; - final String nonce; - final String to; - final int? transactionIndex; - final String value; - final String type; - final List accessList; - final String chainId; - final String v; - final String r; - final String s; - - PendingEthTxDto copyWith({ - String? blockHash, - int? blockNumber, - String? from, - String? gas, - String? maxPriorityFeePerGas, - String? maxFeePerGas, - String? hash, - String? input, - String? nonce, - String? to, - int? transactionIndex, - String? value, - String? type, - List? accessList, - String? chainId, - String? v, - String? r, - String? s, - }) => - PendingEthTxDto( - blockHash: blockHash ?? this.blockHash, - blockNumber: blockNumber ?? this.blockNumber, - from: from ?? this.from, - gas: gas ?? this.gas, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, - hash: hash ?? this.hash, - input: input ?? this.input, - nonce: nonce ?? this.nonce, - to: to ?? this.to, - transactionIndex: transactionIndex ?? this.transactionIndex, - value: value ?? this.value, - type: type ?? this.type, - accessList: accessList ?? this.accessList, - chainId: chainId ?? this.chainId, - v: v ?? this.v, - r: r ?? this.r, - s: s ?? this.s, - ); - - Map toMap() { - final map = {}; - map['blockHash'] = blockHash; - map['blockNumber'] = blockNumber; - map['from'] = from; - map['gas'] = gas; - map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; - map['maxFeePerGas'] = maxFeePerGas; - map['hash'] = hash; - map['input'] = input; - map['nonce'] = nonce; - map['to'] = to; - map['transactionIndex'] = transactionIndex; - map['value'] = value; - map['type'] = type; - map['accessList'] = accessList; - map['chainId'] = chainId; - map['v'] = v; - map['r'] = r; - map['s'] = s; - return map; - } - - @override - String toString() { - return toMap().toString(); - } -} 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/main.dart b/lib/main.dart index ccf2ecf6e..ba3583668 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -200,6 +200,7 @@ void main(List args) async { await Logging.instance.initialize( (await StackFileSystem.applicationLogsDirectory(Prefs.instance)).path, level: Prefs.instance.logLevel, + debugConsoleLevel: kDebugMode ? Level.trace : null, ); await xelis_api.setUpRustLogger(); diff --git a/lib/models/exchange/aggregate_currency.dart b/lib/models/exchange/aggregate_currency.dart index 841262c1d..ffaccd3c0 100644 --- a/lib/models/exchange/aggregate_currency.dart +++ b/lib/models/exchange/aggregate_currency.dart @@ -8,9 +8,10 @@ * */ +import 'package:tuple/tuple.dart'; + import '../isar/exchange_cache/currency.dart'; import '../isar/exchange_cache/pair.dart'; -import 'package:tuple/tuple.dart'; class AggregateCurrency { final Map _map = {}; @@ -29,6 +30,8 @@ class AggregateCurrency { return _map[exchangeName]; } + String? networkFor(String exchangeName) => forExchange(exchangeName)?.network; + String get ticker => _map.values.first!.ticker; String get name => _map.values.first!.name; diff --git a/lib/models/exchange/change_now/cn_exchange_transaction.dart b/lib/models/exchange/change_now/cn_exchange_transaction.dart new file mode 100644 index 000000000..5063df995 --- /dev/null +++ b/lib/models/exchange/change_now/cn_exchange_transaction.dart @@ -0,0 +1,174 @@ +/* + * 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-04-26 + * + */ + +import 'package:decimal/decimal.dart'; +import 'package:uuid/uuid.dart'; + +import 'cn_exchange_transaction_status.dart'; + +class CNExchangeTransaction { + /// The amount being sent from the user. + final Decimal fromAmount; + + /// The amount the user will receive. + final Decimal toAmount; + + /// The type of exchange flow. Either "standard" or "fixed-rate". + final String flow; + + /// Direction of the exchange: "direct" or "reverse". + final String type; + + /// The address to which the user sends the input currency. + final String payinAddress; + + /// The address where the exchanged currency will be sent. + final String payoutAddress; + + /// Extra ID for payout address (e.g., memo, tag). Empty string if not used. + final String payoutExtraId; + + /// Currency ticker being exchanged from (e.g., "btc"). + final String fromCurrency; + + /// Currency ticker being exchanged to (e.g., "xmr"). + final String toCurrency; + + /// Refund address in case of failure or timeout. + final String refundAddress; + + /// Extra ID for the refund address (if needed). Empty string if not used. + final String refundExtraId; + + /// Deadline until which the estimated rate or transaction is valid. + final DateTime validUntil; + + /// Date when transaction was created. + final DateTime date; + + /// Unique transaction identifier. + final String id; + + /// The user-defined or system-determined amount in a "directed" flow. + final Decimal? directedAmount; + + /// Network of the currency being sent. + final String fromNetwork; + + /// Network of the currency being received. + final String toNetwork; + + final String uuid; + + final CNExchangeTransactionStatus? statusObject; + + const CNExchangeTransaction({ + required this.fromAmount, + required this.toAmount, + required this.flow, + required this.type, + required this.payinAddress, + required this.payoutAddress, + required this.payoutExtraId, + required this.fromCurrency, + required this.toCurrency, + required this.refundAddress, + required this.refundExtraId, + required this.validUntil, + required this.date, + required this.id, + required this.directedAmount, + required this.fromNetwork, + required this.toNetwork, + required this.uuid, + this.statusObject, + }); + + factory CNExchangeTransaction.fromJson(Map json) { + return CNExchangeTransaction( + fromAmount: Decimal.parse(json["fromAmount"].toString()), + toAmount: Decimal.parse(json["toAmount"].toString()), + flow: json["flow"] as String, + type: json["type"] as String, + payinAddress: json["payinAddress"] as String, + payoutAddress: json["payoutAddress"] as String, + payoutExtraId: json["payoutExtraId"] as String? ?? "", + fromCurrency: json["fromCurrency"] as String, + toCurrency: json["toCurrency"] as String, + refundAddress: json["refundAddress"] as String, + refundExtraId: json["refundExtraId"] as String, + validUntil: DateTime.parse(json["validUntil"] as String), + date: DateTime.parse(json["date"] as String), + id: json["id"] as String, + directedAmount: Decimal.tryParse(json["directedAmount"].toString()), + fromNetwork: json["fromNetwork"] as String? ?? "", + toNetwork: json["toNetwork"] as String? ?? "", + uuid: json["uuid"] as String? ?? const Uuid().v1(), + statusObject: + json["statusObject"] is Map + ? CNExchangeTransactionStatus.fromMap( + json["statusObject"] as Map, + ) + : null, + ); + } + + Map toMap() { + return { + "fromAmount": fromAmount, + "toAmount": toAmount, + "flow": flow, + "type": type, + "payinAddress": payinAddress, + "payoutAddress": payoutAddress, + "payoutExtraId": payoutExtraId, + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "refundAddress": refundAddress, + "refundExtraId": refundExtraId, + "validUntil": validUntil.toIso8601String(), + "date": date.toIso8601String(), + "id": id, + "directedAmount": directedAmount, + "fromNetwork": fromNetwork, + "uuid": uuid, + "statusObject": statusObject?.toMap(), + }; + } + + CNExchangeTransaction copyWithStatus(CNExchangeTransactionStatus? status) { + return CNExchangeTransaction( + fromAmount: fromAmount, + toAmount: toAmount, + flow: flow, + type: type, + payinAddress: payinAddress, + payoutAddress: payoutAddress, + payoutExtraId: payoutExtraId, + fromCurrency: fromCurrency, + toCurrency: toCurrency, + refundAddress: refundAddress, + refundExtraId: refundExtraId, + validUntil: validUntil, + date: date, + id: id, + directedAmount: directedAmount, + fromNetwork: fromNetwork, + toNetwork: toNetwork, + uuid: uuid, + statusObject: status, + ); + } + + @override + String toString() { + return "CNExchangeTransaction: ${toMap()}"; + } +} diff --git a/lib/models/exchange/change_now/cn_exchange_transaction_status.dart b/lib/models/exchange/change_now/cn_exchange_transaction_status.dart new file mode 100644 index 000000000..14d7c03e5 --- /dev/null +++ b/lib/models/exchange/change_now/cn_exchange_transaction_status.dart @@ -0,0 +1,172 @@ +import 'package:decimal/decimal.dart'; + +enum ChangeNowTransactionStatus { + New, + Waiting, + Confirming, + Exchanging, + Sending, + Finished, + Failed, + Refunded, + Verifying, +} + +extension ChangeNowTransactionStatusExt on ChangeNowTransactionStatus { + String get lowerCaseName => name.toLowerCase(); +} + +ChangeNowTransactionStatus changeNowTransactionStatusFromStringIgnoreCase( + String string, +) { + for (final value in ChangeNowTransactionStatus.values) { + if (value.lowerCaseName == string.toLowerCase()) { + return value; + } + } + throw ArgumentError( + "String value does not match any known ChangeNowTransactionStatus", + ); +} + +class CNExchangeTransactionStatus { + final String id; + final ChangeNowTransactionStatus status; + final bool actionsAvailable; + final String fromCurrency; + final String fromNetwork; + final String toCurrency; + final String toNetwork; + final String? expectedAmountFrom; + final String? expectedAmountTo; + final String? amountFrom; + final String? amountTo; + final String payinAddress; + final String payoutAddress; + final String? payinExtraId; + final String? payoutExtraId; + final String? refundAddress; + final String? refundExtraId; + final String createdAt; + final String updatedAt; + final String? depositReceivedAt; + final String? payinHash; + final String? payoutHash; + final String fromLegacyTicker; + final String toLegacyTicker; + final String? refundHash; + final String? refundAmount; + final int? userId; + final String? validUntil; + + const CNExchangeTransactionStatus({ + required this.id, + required this.status, + required this.actionsAvailable, + required this.fromCurrency, + required this.fromNetwork, + required this.toCurrency, + required this.toNetwork, + this.expectedAmountFrom, + this.expectedAmountTo, + this.amountFrom, + this.amountTo, + required this.payinAddress, + required this.payoutAddress, + this.payinExtraId, + this.payoutExtraId, + this.refundAddress, + this.refundExtraId, + required this.createdAt, + required this.updatedAt, + this.depositReceivedAt, + this.payinHash, + this.payoutHash, + required this.fromLegacyTicker, + required this.toLegacyTicker, + this.refundHash, + this.refundAmount, + this.userId, + this.validUntil, + }); + + factory CNExchangeTransactionStatus.fromMap(Map map) { + return CNExchangeTransactionStatus( + id: map["id"] as String, + status: changeNowTransactionStatusFromStringIgnoreCase( + map["status"] as String, + ), + actionsAvailable: map["actionsAvailable"] as bool, + fromCurrency: map["fromCurrency"] as String? ?? "", + fromNetwork: map["fromNetwork"] as String? ?? "", + toCurrency: map["toCurrency"] as String? ?? "", + toNetwork: map["toNetwork"] as String? ?? "", + expectedAmountFrom: _get(map["expectedAmountFrom"]), + expectedAmountTo: _get(map["expectedAmountTo"]), + amountFrom: _get(map["amountFrom"]), + amountTo: _get(map["amountTo"]), + payinAddress: map["payinAddress"] as String? ?? "", + payoutAddress: map["payoutAddress"] as String? ?? "", + payinExtraId: map["payinExtraId"] as String?, + payoutExtraId: map["payoutExtraId"] as String?, + refundAddress: map["refundAddress"] as String?, + refundExtraId: map["refundExtraId"] as String?, + createdAt: map["createdAt"] as String? ?? "", + updatedAt: map["updatedAt"] as String? ?? "", + depositReceivedAt: map["depositReceivedAt"] as String?, + payinHash: map["payinHash"] as String?, + payoutHash: map["payoutHash"] as String?, + fromLegacyTicker: map["fromLegacyTicker"] as String? ?? "", + toLegacyTicker: map["toLegacyTicker"] as String? ?? "", + refundHash: map["refundHash"] as String?, + refundAmount: _get(map["refundAmount"]), + userId: + map["userId"] is int + ? map["userId"] as int + : int.tryParse(map["userId"].toString()), + validUntil: map["validUntil"] as String?, + ); + } + + Map toMap() { + return { + "id": id, + "status": status, + "actionsAvailable": actionsAvailable, + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork, + "toCurrency": toCurrency, + "toNetwork": toNetwork, + "expectedAmountFrom": expectedAmountFrom, + "expectedAmountTo": expectedAmountTo, + "amountFrom": amountFrom, + "amountTo": amountTo, + "payinAddress": payinAddress, + "payoutAddress": payoutAddress, + "payinExtraId": payinExtraId, + "payoutExtraId": payoutExtraId, + "refundAddress": refundAddress, + "refundExtraId": refundExtraId, + "createdAt": createdAt, + "updatedAt": updatedAt, + "depositReceivedAt": depositReceivedAt, + "payinHash": payinHash, + "payoutHash": payoutHash, + "fromLegacyTicker": fromLegacyTicker, + "toLegacyTicker": toLegacyTicker, + "refundHash": refundHash, + "refundAmount": refundAmount, + "userId": userId, + "validUntil": validUntil, + }; + } + + static String? _get(dynamic value) { + if (value is String) return value; + if (value is num) return Decimal.tryParse(value.toString())?.toString(); + return null; + } + + @override + String toString() => "CNExchangeTransactionStatus: ${toMap()}"; +} diff --git a/lib/models/exchange/change_now/estimated_exchange_amount.dart b/lib/models/exchange/change_now/estimated_exchange_amount.dart index 62eee2f60..7648cdacb 100644 --- a/lib/models/exchange/change_now/estimated_exchange_amount.dart +++ b/lib/models/exchange/change_now/estimated_exchange_amount.dart @@ -1,96 +1,142 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ +import "package:decimal/decimal.dart"; -import 'package:decimal/decimal.dart'; - -import '../../../utilities/logger.dart'; +import "../../../services/exchange/change_now/change_now_api.dart"; +/// Immutable model representing exchange rate information. class EstimatedExchangeAmount { - /// Estimated exchange amount - final Decimal estimatedAmount; + /// Ticker of the currency you want to exchange. + final String fromCurrency; - /// Dash-separated min and max estimated time in minutes - final String transactionSpeedForecast; + /// Network of the currency you want to exchange. + final String fromNetwork; - /// Some warnings like warnings that transactions on this network - /// take longer or that the currency has moved to another network - final String? warningMessage; + /// Ticker of the currency you want to receive. + final String toCurrency; + + /// Network of the currency you want to receive. + final String toNetwork; - /// (Optional) Use rateId for fixed-rate flow. If this field is true, you - /// could use returned field "rateId" in next method for creating transaction - /// to freeze estimated amount that you got in this method. Current estimated - /// amount would be valid until time in field "validUntil" + /// Type of exchange flow. Either `standard` or `fixed-rate`. + final CNFlow flow; + + /// Direction of exchange flow. Either `direct` or `reverse`. + /// + /// - `direct`: set amount for `fromCurrency`, get amount of `toCurrency`. + /// - `reverse`: set amount for `toCurrency`, get amount of `fromCurrency`. + final CNExchangeType type; + + /// RateId is needed for fixed-rate flow. Used to freeze estimated amount. final String? rateId; - /// ONLY for fixed rate. - /// Network fee for transferring funds between wallets, it should be deducted - /// from the result. Formula for calculating the estimated amount is given below - /// estimatedAmount = (rate * amount) - networkFee - final Decimal? networkFee; + /// Date and time before which the estimated amount is valid if using `rateId`. + final DateTime validUntil; + + /// Dash-separated min and max estimated time in minutes. + final String? transactionSpeedForecast; + + /// Some warnings, such as if a currency has moved to another network or transactions take longer. + final String? warningMessage; + + /// Deposit fee in the selected currency. + final Decimal depositFee; + + /// Withdrawal fee in the selected currency. + final Decimal withdrawalFee; + + /// A personal and permanent identifier under which information is stored in the database. + /// + /// Only enabled for special partners. + final String? userId; + + /// Exchange amount of `fromCurrency`. + /// + /// If `type=reverse`, this is an estimated value. + final Decimal fromAmount; - EstimatedExchangeAmount({ - required this.estimatedAmount, - required this.transactionSpeedForecast, - required this.warningMessage, + /// Exchange amount of `toCurrency`. + /// + /// If `type=direct`, this is an estimated value. + final Decimal toAmount; + + /// Creates an immutable [EstimatedExchangeAmount] instance. + const EstimatedExchangeAmount({ + required this.fromCurrency, + required this.fromNetwork, + required this.toCurrency, + required this.toNetwork, + required this.flow, + required this.type, required this.rateId, - this.networkFee, + required this.validUntil, + this.transactionSpeedForecast, + this.warningMessage, + required this.depositFee, + required this.withdrawalFee, + this.userId, + required this.fromAmount, + required this.toAmount, }); + /// Creates an instance of [EstimatedExchangeAmount] from a JSON map. factory EstimatedExchangeAmount.fromJson(Map json) { - try { - return EstimatedExchangeAmount( - estimatedAmount: Decimal.parse( - json["estimatedAmount"]?.toString() ?? - json["estimatedDeposit"].toString(), - ), - transactionSpeedForecast: - json["transactionSpeedForecast"] as String? ?? "", - warningMessage: json["warningMessage"] as String?, - rateId: json["rateId"] as String?, - networkFee: Decimal.tryParse(json["networkFee"].toString()), - ); - } catch (e, s) { - Logging.instance.e( - "Failed to parse: $json", - error: e, - stackTrace: s, - ); - rethrow; - } + return EstimatedExchangeAmount( + fromCurrency: json["fromCurrency"] as String, + fromNetwork: json["fromNetwork"] as String, + toCurrency: json["toCurrency"] as String, + toNetwork: json["toNetwork"] as String, + flow: _parseFlow(json["flow"] as String), + type: _parseType(json["type"] as String), + rateId: json["rateId"] as String?, + validUntil: DateTime.parse(json["validUntil"] as String), + transactionSpeedForecast: json["transactionSpeedForecast"] as String?, + warningMessage: json["warningMessage"] as String?, + depositFee: Decimal.parse(json["depositFee"].toString()), + withdrawalFee: Decimal.parse(json["withdrawalFee"].toString()), + userId: json["userId"]?.toString(), + fromAmount: Decimal.parse(json["fromAmount"].toString()), + toAmount: Decimal.parse(json["toAmount"].toString()), + ); } + /// Converts this [EstimatedExchangeAmount] instance to a JSON map. Map toJson() { return { - "estimatedAmount": estimatedAmount, + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork, + "toCurrency": toCurrency, + "toNetwork": toNetwork, + "flow": flow.name.replaceAll("fixedRate", "fixed-rate"), + "type": type.name, + "rateId": rateId, + "validUntil": validUntil.toIso8601String(), "transactionSpeedForecast": transactionSpeedForecast, "warningMessage": warningMessage, - "rateId": rateId, - "networkFee": networkFee, + "depositFee": depositFee.toString(), + "withdrawalFee": withdrawalFee.toString(), + "userId": userId, + "fromAmount": fromAmount.toString(), + "toAmount": toAmount.toString(), }; } - EstimatedExchangeAmount copyWith({ - Decimal? estimatedAmount, - String? transactionSpeedForecast, - String? warningMessage, - String? rateId, - Decimal? networkFee, - }) { - return EstimatedExchangeAmount( - estimatedAmount: estimatedAmount ?? this.estimatedAmount, - transactionSpeedForecast: - transactionSpeedForecast ?? this.transactionSpeedForecast, - warningMessage: warningMessage ?? this.warningMessage, - rateId: rateId ?? this.rateId, - networkFee: networkFee ?? this.networkFee, - ); + static CNFlow _parseFlow(String value) { + switch (value) { + case "fixed-rate": + return CNFlow.fixedRate; + case "standard": + default: + return CNFlow.standard; + } + } + + static CNExchangeType _parseType(String value) { + switch (value) { + case "reverse": + return CNExchangeType.reverse; + case "direct": + default: + return CNExchangeType.direct; + } } @override diff --git a/lib/models/exchange/change_now/exchange_transaction.dart b/lib/models/exchange/change_now/exchange_transaction.dart index 6bd514546..1de49bfd0 100644 --- a/lib/models/exchange/change_now/exchange_transaction.dart +++ b/lib/models/exchange/change_now/exchange_transaction.dart @@ -1,6 +1,6 @@ -/* +/* * 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. @@ -77,6 +77,7 @@ class ExchangeTransaction { // @HiveField(14) final ExchangeTransactionStatus? statusObject; + @Deprecated("Only kept for legacy reasons") ExchangeTransaction({ required this.id, required this.payinAddress, @@ -96,6 +97,7 @@ class ExchangeTransaction { }); /// Important to pass a "date": DateTime in or it will default to 1970 + @Deprecated("Only kept for legacy reasons") factory ExchangeTransaction.fromJson(Map json) { try { return ExchangeTransaction( @@ -111,14 +113,16 @@ class ExchangeTransaction { refundExtraId: json["refundExtraId"] as String? ?? "", payoutExtraIdName: json["payoutExtraIdName"] as String? ?? "", uuid: json["uuid"] as String? ?? const Uuid().v1(), - date: DateTime.tryParse(json["date"] as String? ?? "") ?? + date: + DateTime.tryParse(json["date"] as String? ?? "") ?? DateTime.fromMillisecondsSinceEpoch(0), statusString: json["statusString"] as String? ?? "", - statusObject: json["statusObject"] is Map - ? ExchangeTransactionStatus.fromJson( - json["statusObject"] as Map, - ) - : null, + statusObject: + json["statusObject"] is Map + ? ExchangeTransactionStatus.fromJson( + json["statusObject"] as Map, + ) + : null, ); } catch (e) { rethrow; diff --git a/lib/models/exchange/change_now/exchange_transaction_status.dart b/lib/models/exchange/change_now/exchange_transaction_status.dart index 7874b8b80..5a904690f 100644 --- a/lib/models/exchange/change_now/exchange_transaction_status.dart +++ b/lib/models/exchange/change_now/exchange_transaction_status.dart @@ -1,6 +1,6 @@ -/* +/* * 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. @@ -11,38 +11,14 @@ import 'package:hive/hive.dart'; import '../../../utilities/logger.dart'; +import 'cn_exchange_transaction_status.dart' + show + ChangeNowTransactionStatus, + changeNowTransactionStatusFromStringIgnoreCase; part '../../type_adaptors/exchange_transaction_status.g.dart'; -enum ChangeNowTransactionStatus { - New, - Waiting, - Confirming, - Exchanging, - Sending, - Finished, - Failed, - Refunded, - Verifying, -} - -extension ChangeNowTransactionStatusExt on ChangeNowTransactionStatus { - String get lowerCaseName => name.toLowerCase(); -} - -ChangeNowTransactionStatus changeNowTransactionStatusFromStringIgnoreCase( - String string, -) { - for (final value in ChangeNowTransactionStatus.values) { - if (value.lowerCaseName == string.toLowerCase()) { - return value; - } - } - throw ArgumentError( - "String value does not match any known ChangeNowTransactionStatus", - ); -} - +@Deprecated("Only kept for legacy reasons") @HiveType(typeId: 16) class ExchangeTransactionStatus { /// Transaction status @@ -156,6 +132,7 @@ class ExchangeTransactionStatus { @HiveField(26) final bool isPartner; + @Deprecated("Only kept for legacy reasons") ExchangeTransactionStatus({ required this.status, required this.payinAddress, @@ -186,6 +163,7 @@ class ExchangeTransactionStatus { required this.payload, }); + @Deprecated("Only kept for legacy reasons") factory ExchangeTransactionStatus.fromJson(Map json) { Logging.instance.d(json, stackTrace: StackTrace.current); try { @@ -199,12 +177,14 @@ class ExchangeTransactionStatus { toCurrency: json["toCurrency"] as String? ?? "", id: json["id"] as String, updatedAt: json["updatedAt"] as String? ?? "", - expectedSendAmountDecimal: json["expectedSendAmount"] == null - ? "" - : json["expectedSendAmount"].toString(), - expectedReceiveAmountDecimal: json["expectedReceiveAmount"] == null - ? "" - : json["expectedReceiveAmount"].toString(), + expectedSendAmountDecimal: + json["expectedSendAmount"] == null + ? "" + : json["expectedSendAmount"].toString(), + expectedReceiveAmountDecimal: + json["expectedReceiveAmount"] == null + ? "" + : json["expectedReceiveAmount"].toString(), createdAt: json["createdAt"] as String? ?? "", isPartner: json["isPartner"] as bool, depositReceivedAt: json["depositReceivedAt"] as String? ?? "", @@ -216,9 +196,10 @@ class ExchangeTransactionStatus { payoutExtraId: json["payoutExtraId"] as String? ?? "", amountSendDecimal: json["amountSend"] == null ? "" : json["amountSend"].toString(), - amountReceiveDecimal: json["amountReceive"] == null - ? "" - : json["amountReceive"].toString(), + amountReceiveDecimal: + json["amountReceive"] == null + ? "" + : json["amountReceive"].toString(), tokensDestination: json["tokensDestination"] as String? ?? "", refundAddress: json["refundAddress"] as String? ?? "", refundExtraId: json["refundExtraId"] as String? ?? "", @@ -233,6 +214,7 @@ class ExchangeTransactionStatus { } } + @Deprecated("Only kept for legacy reasons") Map toJson() { final map = { "status": status.name, @@ -267,6 +249,7 @@ class ExchangeTransactionStatus { return map; } + @Deprecated("Only kept for legacy reasons") ExchangeTransactionStatus copyWith({ ChangeNowTransactionStatus? status, String? payinAddress, diff --git a/lib/models/exchange/incomplete_exchange.dart b/lib/models/exchange/incomplete_exchange.dart index 1397afb1a..3608b8b05 100644 --- a/lib/models/exchange/incomplete_exchange.dart +++ b/lib/models/exchange/incomplete_exchange.dart @@ -10,13 +10,16 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; + +import '../../utilities/enums/exchange_rate_type_enum.dart'; import 'response_objects/estimate.dart'; import 'response_objects/trade.dart'; -import '../../utilities/enums/exchange_rate_type_enum.dart'; class IncompleteExchangeModel extends ChangeNotifier { final String sendTicker; + final String? sendNetwork; final String receiveTicker; + final String? receiveNetwork; final String rateInfo; @@ -74,7 +77,9 @@ class IncompleteExchangeModel extends ChangeNotifier { IncompleteExchangeModel({ required this.sendTicker, + required this.sendNetwork, required this.receiveTicker, + required this.receiveNetwork, required this.rateInfo, required this.sendAmount, required this.receiveAmount, diff --git a/lib/models/exchange/response_objects/trade.dart b/lib/models/exchange/response_objects/trade.dart index 03685a0ab..e6f996f9b 100644 --- a/lib/models/exchange/response_objects/trade.dart +++ b/lib/models/exchange/response_objects/trade.dart @@ -11,6 +11,7 @@ import 'package:hive/hive.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; +import '../change_now/cn_exchange_transaction.dart'; import '../change_now/exchange_transaction.dart'; part 'trade.g.dart'; @@ -225,9 +226,10 @@ class Trade { timestamp: exTx.date, updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date, payInCurrency: exTx.fromCurrency, - payInAmount: exTx.statusObject!.amountSendDecimal.isEmpty - ? exTx.statusObject!.expectedSendAmountDecimal - : exTx.statusObject!.amountSendDecimal, + payInAmount: + exTx.statusObject!.amountSendDecimal.isEmpty + ? exTx.statusObject!.expectedSendAmountDecimal + : exTx.statusObject!.amountSendDecimal, payInAddress: exTx.payinAddress, payInNetwork: "", payInExtraId: exTx.payinExtraId, @@ -245,6 +247,42 @@ class Trade { ); } + factory Trade.fromCNExchangeTransaction( + CNExchangeTransaction exTx, + bool reversed, + ) { + return Trade( + uuid: exTx.uuid, + tradeId: exTx.id, + rateType: "", + direction: reversed ? "reverse" : "direct", + timestamp: exTx.date, + updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date, + payInCurrency: exTx.fromCurrency, + payInAmount: + exTx.statusObject!.amountFrom ?? + exTx.statusObject!.expectedAmountFrom ?? + exTx.fromAmount.toString(), + payInAddress: exTx.payinAddress, + payInNetwork: exTx.fromNetwork, + payInExtraId: "", + payInTxid: exTx.statusObject!.payinHash ?? "", + payOutCurrency: exTx.toCurrency, + payOutAmount: + exTx.statusObject!.amountTo ?? + exTx.statusObject!.expectedAmountTo ?? + exTx.toAmount.toString(), + payOutAddress: exTx.payoutAddress, + payOutNetwork: exTx.toNetwork, + payOutExtraId: exTx.payoutExtraId, + payOutTxid: exTx.statusObject!.payoutHash ?? "", + refundAddress: exTx.refundAddress, + refundExtraId: exTx.refundExtraId, + status: exTx.statusObject!.status.name, + exchangeName: ChangeNowExchange.exchangeName, + ); + } + @override String toString() { return toMap().toString(); diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index b7a92c26e..378e93d0f 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -135,8 +135,9 @@ class TransactionV2 { return Amount.zeroWith(fractionDigits: fractionDigits); } - final inSum = - inputs.map((e) => e.value).reduce((value, element) => value += element); + final inSum = inputs + .map((e) => e.value) + .reduce((value, element) => value += element); final outSum = outputs .map((e) => e.value) .reduce((value, element) => value += element); @@ -161,15 +162,20 @@ class TransactionV2 { } Amount getAmountSparkSelfMinted({required int fractionDigits}) { - final outSum = outputs.where((e) { - final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first; - return e.walletOwns && (op == OP_SPARKMINT); - }).fold(BigInt.zero, (p, e) => p + e.value); + final outSum = outputs + .where((e) { + final op = e.scriptPubKeyHex.substring(0, 2).toUint8ListFromHex.first; + return e.walletOwns && (op == OP_SPARKMINT); + }) + .fold(BigInt.zero, (p, e) => p + e.value); return Amount(rawValue: outSum, fractionDigits: fractionDigits); } - Amount getAmountSentFromThisWallet({required int fractionDigits}) { + Amount getAmountSentFromThisWallet({ + required int fractionDigits, + required bool subtractFee, + }) { if (_isMonero()) { if (type == TransactionType.outgoing) { return _getMoneroAmount()!; @@ -182,15 +188,11 @@ class TransactionV2 { .where((e) => e.walletOwns) .fold(BigInt.zero, (p, e) => p + e.value); - Amount amount = Amount( - rawValue: inSum, - fractionDigits: fractionDigits, - ) - - getAmountReceivedInThisWallet( - fractionDigits: fractionDigits, - ); + Amount amount = + Amount(rawValue: inSum, fractionDigits: fractionDigits) - + getAmountReceivedInThisWallet(fractionDigits: fractionDigits); - if (subType != TransactionSubType.ethToken) { + if (subtractFee) { amount = amount - getFee(fractionDigits: fractionDigits); } @@ -204,9 +206,9 @@ class TransactionV2 { } Set associatedAddresses() => { - ...inputs.map((e) => e.addresses).expand((e) => e), - ...outputs.map((e) => e.addresses).expand((e) => e), - }; + ...inputs.map((e) => e.addresses).expand((e) => e), + ...outputs.map((e) => e.addresses).expand((e) => e), + }; Amount? _getOverrideFee() { try { @@ -238,7 +240,8 @@ class TransactionV2 { required int minConfirms, required int minCoinbaseConfirms, }) { - String prettyConfirms() => "(" + String prettyConfirms() => + "(" "${getConfirmations(currentChainHeight)}" "/" "${(isCoinbase() ? minCoinbaseConfirms : minConfirms)}" diff --git a/lib/models/paymint/fee_object_model.dart b/lib/models/paymint/fee_object_model.dart index 3745f997d..0c00f807f 100644 --- a/lib/models/paymint/fee_object_model.dart +++ b/lib/models/paymint/fee_object_model.dart @@ -9,9 +9,9 @@ */ class FeeObject { - final int fast; - final int medium; - final int slow; + final BigInt fast; + final BigInt medium; + final BigInt slow; final int numberOfBlocksFast; final int numberOfBlocksAverage; @@ -26,19 +26,22 @@ class FeeObject { required this.slow, }); - factory FeeObject.fromJson(Map json) { - return FeeObject( - fast: json['fast'] as int, - medium: json['average'] as int, - slow: json['slow'] as int, - numberOfBlocksFast: json['numberOfBlocksFast'] as int, - numberOfBlocksAverage: json['numberOfBlocksAverage'] as int, - numberOfBlocksSlow: json['numberOfBlocksSlow'] as int, - ); - } - @override String toString() { return "{fast: $fast, medium: $medium, slow: $slow, numberOfBlocksFast: $numberOfBlocksFast, numberOfBlocksAverage: $numberOfBlocksAverage, numberOfBlocksSlow: $numberOfBlocksSlow}"; } } + +class EthFeeObject extends FeeObject { + final BigInt suggestBaseFee; + + EthFeeObject({ + required this.suggestBaseFee, + required super.numberOfBlocksFast, + required super.numberOfBlocksAverage, + required super.numberOfBlocksSlow, + required super.fast, + required super.medium, + required super.slow, + }); +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart index 89a818529..23035dc8e 100644 --- a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -48,26 +48,28 @@ class _AddTokenListElementState extends ConsumerState { @override Widget build(BuildContext context) { - final currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - widget.data.token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); + final currency = + ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.data.token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); final String mainLabel = widget.data.token.name; final double iconSize = isDesktop ? 32 : 24; return RoundedWhiteContainer( padding: EdgeInsets.all(isDesktop ? 16 : 12), - borderColor: isDesktop - ? Theme.of(context).extension()!.backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.backgroundAppBar + : null, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -75,71 +77,68 @@ class _AddTokenListElementState extends ConsumerState { children: [ currency != null ? SvgPicture.network( - currency.image, - width: iconSize, - height: iconSize, - placeholderBuilder: (_) => AppIcon( - width: iconSize, - height: iconSize, - ), - ) + currency.image, + width: iconSize, + height: iconSize, + placeholderBuilder: + (_) => AppIcon(width: iconSize, height: iconSize), + ) : SvgPicture.asset( - widget.data.token.symbol == "BNB" - ? Assets.svg.bnbIcon - : Assets.svg.ethereum, - width: iconSize, - height: iconSize, - ), - const SizedBox( - width: 12, - ), + widget.data.token.symbol == "BNB" + ? Assets.svg.bnbIcon + : Assets.svg.ethereum, + width: iconSize, + height: iconSize, + ), + const SizedBox(width: 12), ConditionalParent( condition: isDesktop, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const SizedBox( - height: 2, - ), - Text( - widget.data.token.symbol, - style: STextStyles.desktopTextExtraExtraSmall(context), - overflow: TextOverflow.ellipsis, + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const SizedBox(height: 2), + Text( + widget.data.token.symbol, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + overflow: TextOverflow.ellipsis, + ), + ], ), - ], - ), child: Text( isDesktop ? mainLabel : "$mainLabel (${widget.data.token.symbol})", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.w600_14(context), + style: + isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w600_14(context), overflow: TextOverflow.ellipsis, ), ), ], ), - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), isDesktop ? Checkbox( - value: widget.data.selected, - onChanged: (newValue) => - setState(() => widget.data.selected = newValue!), - ) + value: widget.data.selected, + onChanged: + (newValue) => + setState(() => widget.data.selected = newValue!), + ) : SizedBox( - height: 20, - width: 40, - child: DraggableSwitchButton( - isOn: widget.data.selected, - onValueChanged: (newValue) { - widget.data.selected = newValue; - }, - ), + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: widget.data.selected, + onValueChanged: (newValue) { + widget.data.selected = newValue; + }, ), + ), ], ), ); diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 68527ccaf..80acac3ed 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.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/isar/exchange_cache/currency.dart'; @@ -28,10 +29,7 @@ import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; class CoinSelectItem extends ConsumerWidget { - const CoinSelectItem({ - super.key, - required this.entity, - }); + const CoinSelectItem({super.key, required this.entity}); final AddWalletListEntity entity; @@ -44,82 +42,77 @@ class CoinSelectItem extends ConsumerWidget { String? tokenImageUri; if (entity is EthTokenEntity) { - final currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - (entity as EthTokenEntity).token.address, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); + final currency = + ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + (entity as EthTokenEntity).token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); tokenImageUri = currency?.image; } return Container( decoration: BoxDecoration( - color: selectedEntity == entity - ? Theme.of(context).extension()!.textFieldActiveBG - : Theme.of(context).extension()!.popupBG, - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + color: + selectedEntity == entity + ? Theme.of(context).extension()!.textFieldActiveBG + : Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: MaterialButton( key: Key("coinSelectItemButtonKey_${entity.name}${entity.ticker}"), - padding: isDesktop - ? const EdgeInsets.only(left: 24) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(12), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: isDesktop ? 70 : 0, - ), + constraints: BoxConstraints(minHeight: isDesktop ? 70 : 0), child: Row( children: [ tokenImageUri != null - ? SvgPicture.network( - tokenImageUri, - width: 26, - height: 26, - ) + ? SvgPicture.network(tokenImageUri, width: 26, height: 26) : SvgPicture.file( - File( - ref.watch(coinIconProvider(entity.cryptoCurrency)), - ), - width: 26, - height: 26, - ), - SizedBox( - width: isDesktop ? 12 : 10, - ), + File(ref.watch(coinIconProvider(entity.cryptoCurrency))), + width: 26, + height: 26, + ), + SizedBox(width: isDesktop ? 12 : 10), Text( "${entity.name} (${entity.ticker})", - style: isDesktop - ? STextStyles.desktopTextMedium(context) - : STextStyles.subtitle600(context).copyWith( - fontSize: 14, - ), + style: + isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.subtitle600( + context, + ).copyWith(fontSize: 14), ), if (isDesktop && selectedEntity == entity) const Spacer(), if (isDesktop && selectedEntity == entity) Padding( - padding: const EdgeInsets.only( - right: 18, - ), + padding: const EdgeInsets.only(right: 18), child: SizedBox( width: 24, height: 24, child: SvgPicture.asset( Assets.svg.check, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 239b56db4..d28bc8277 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -19,7 +19,6 @@ import '../../models/isar/models/isar_models.dart'; import '../../models/trade_wallet_lookup.dart'; import '../../notifications/show_flush_bar.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; -import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; @@ -564,24 +563,32 @@ class _ConfirmChangeNowSendViewState (value) => value.getPrice(coin), ), ); - final amountWithoutChange = - widget.txData.amountWithoutChange!; - final value = (price.item1 * - amountWithoutChange.decimal) - .toAmount(fractionDigits: 2); - final currency = ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - ); - final locale = ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ); + final String extra; + if (price == null) { + extra = ""; + } else { + final amountWithoutChange = + widget.txData.amountWithoutChange!; + final value = (price.value * + amountWithoutChange.decimal) + .toAmount(fractionDigits: 2); + final currency = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + ); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + + extra = + " | ${value.fiatString(locale: locale)} $currency"; + } return Text( - " | ${value.fiatString(locale: locale)} $currency", + extra, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( diff --git a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart index e3ae98acd..5801cf869 100644 --- a/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart +++ b/lib/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart @@ -15,10 +15,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import '../../../app_config.dart'; -import '../../../exceptions/exchange/unsupported_currency_exception.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; -import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart'; @@ -34,12 +32,9 @@ import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_loading_overlay.dart'; -import '../../../widgets/desktop/primary_button.dart'; -import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/icon_widgets/x_icon.dart'; import '../../../widgets/loading_indicator.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 '../../buy_view/sub_widgets/crypto_selection_view.dart'; @@ -74,26 +69,24 @@ class _ExchangeCurrencySelectionViewState bool _loaded = false; String _searchString = ""; - Future _showUpdatingCurrencies({ - required Future whileFuture, - }) async { + Future _showUpdatingCurrencies({required Future whileFuture}) async { unawaited( showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Loading currencies", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Loading currencies", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -111,85 +104,52 @@ class _ExchangeCurrencySelectionViewState return await _getCurrencies(); } await ExchangeDataLoadingService.instance.initDB(); - final List currencies = await ExchangeDataLoadingService - .instance.isar.currencies - .where() - .filter() - .exchangeNameEqualTo(MajesticBankExchange.exchangeName) - .or() - .exchangeNameStartsWith(TrocadorExchange.exchangeName) - .or() - .exchangeNameStartsWith(NanswapExchange.exchangeName) - .findAll(); - - final cn = await ChangeNowExchange.instance.getPairedCurrencies( - widget.pairedTicker!, - widget.isFixedRate, - ); - - if (cn.value == null) { - if (cn.exception is UnsupportedCurrencyException) { - return _getDistinctCurrenciesFrom(currencies); - } - - if (mounted) { - await showDialog( - context: context, - builder: (context) => StackDialog( - title: "Exchange Error", - message: "Failed to load currency data: ${cn.exception}", - leftButton: SecondaryButton( - label: "Ok", - onPressed: Navigator.of(context, rootNavigator: isDesktop).pop, - ), - rightButton: PrimaryButton( - label: "Retry", - onPressed: () async { - Navigator.of(context, rootNavigator: isDesktop).pop(); - _currencies = await _showUpdatingCurrencies( - whileFuture: _loadCurrencies(), - ); - setState(() {}); - }, - ), - ), - ); - } - } else { - currencies.addAll(cn.value!); - } + final List currencies = + await ExchangeDataLoadingService.instance.isar.currencies + .where() + .filter() + .exchangeNameEqualTo(MajesticBankExchange.exchangeName) + .or() + .exchangeNameStartsWith(TrocadorExchange.exchangeName) + .or() + .exchangeNameStartsWith(NanswapExchange.exchangeName) + .findAll(); return _getDistinctCurrenciesFrom(currencies); } Future> _getCurrencies() async { await ExchangeDataLoadingService.instance.initDB(); - final currencies = await ExchangeDataLoadingService.instance.isar.currencies - .where() - .filter() - .isFiatEqualTo(false) - .and() - .group( - (q) => widget.isFixedRate - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .sortByIsStackCoin() - .thenByName() - .findAll(); + final currencies = + await ExchangeDataLoadingService.instance.isar.currencies + .where() + .filter() + .isFiatEqualTo(false) + .and() + .group( + (q) => + widget.isFixedRate + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .sortByIsStackCoin() + .thenByName() + .findAll(); // If using Tor, filter exchanges which do not support Tor. if (Prefs.instance.useTor) { if (Exchange.exchangeNamesWithTorSupport.isNotEmpty) { currencies.removeWhere( - (element) => !Exchange.exchangeNamesWithTorSupport - .contains(element.exchangeName), + (element) => + !Exchange.exchangeNamesWithTorSupport.contains( + element.exchangeName, + ), ); } } @@ -262,8 +222,9 @@ class _ExchangeCurrencySelectionViewState if (!_loaded) { _loaded = true; WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - _currencies = - await _showUpdatingCurrencies(whileFuture: _loadCurrencies()); + _currencies = await _showUpdatingCurrencies( + whileFuture: _loadCurrencies(), + ); setState(() {}); }); } @@ -284,7 +245,7 @@ class _ExchangeCurrencySelectionViewState const Duration(milliseconds: 50), ); } - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -295,9 +256,7 @@ class _ExchangeCurrencySelectionViewState ), ), body: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: 16), child: child, ), ), @@ -307,10 +266,7 @@ class _ExchangeCurrencySelectionViewState crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, children: [ - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -340,32 +296,31 @@ class _ExchangeCurrencySelectionViewState height: 16, ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Flexible( child: Builder( builder: (context) { @@ -377,17 +332,19 @@ class _ExchangeCurrencySelectionViewState final items = filter(_searchString); - final walletCoins = items - .where( - (currency) => coins - .where( - (coin) => - coin.ticker.toLowerCase() == - currency.ticker.toLowerCase(), - ) - .isNotEmpty, - ) - .toList(); + final walletCoins = + items + .where( + (currency) => + coins + .where( + (coin) => + coin.ticker.toLowerCase() == + currency.ticker.toLowerCase(), + ) + .isNotEmpty, + ) + .toList(); // sort alphabetically by name items.sort((a, b) => a.name.compareTo(b.name)); @@ -408,8 +365,9 @@ class _ExchangeCurrencySelectionViewState primary: isDesktop ? false : null, itemCount: items.length, itemBuilder: (builderContext, index) { - final bool hasImageUrl = - items[index].image.startsWith("http"); + final bool hasImageUrl = items[index].image.startsWith( + "http", + ); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: GestureDetector( @@ -425,29 +383,27 @@ class _ExchangeCurrencySelectionViewState child: AppConfig.isStackCoin(items[index].ticker) ? CoinIconForTicker( - ticker: items[index].ticker, - size: 24, - ) + ticker: items[index].ticker, + size: 24, + ) // ? getIconForTicker( // items[index].ticker, // size: 24, // ) : hasImageUrl - ? SvgPicture.network( - items[index].image, - width: 24, - height: 24, - placeholderBuilder: (_) => - const LoadingIndicator(), - ) - : const SizedBox( - width: 24, - height: 24, - ), - ), - const SizedBox( - width: 10, + ? SvgPicture.network( + items[index].image, + width: 24, + height: 24, + placeholderBuilder: + (_) => const LoadingIndicator(), + ) + : const SizedBox( + width: 24, + height: 24, + ), ), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: @@ -455,19 +411,20 @@ class _ExchangeCurrencySelectionViewState children: [ Text( items[index].name, - style: - STextStyles.largeMedium14(context), - ), - const SizedBox( - height: 2, + style: STextStyles.largeMedium14( + context, + ), ), + const SizedBox(height: 2), Text( items[index].ticker.toUpperCase(), - style: STextStyles.smallMed12(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + style: STextStyles.smallMed12( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textSubtitle1, ), ), ], diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index 111d38448..6976d557b 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -40,6 +40,7 @@ import '../../utilities/amount/amount_unit.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/exchange_rate_type_enum.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -60,12 +61,7 @@ import 'sub_widgets/exchange_provider_options.dart'; import 'sub_widgets/rate_type_toggle.dart'; class ExchangeForm extends ConsumerStatefulWidget { - const ExchangeForm({ - super.key, - this.walletId, - this.coin, - this.contract, - }); + const ExchangeForm({super.key, this.walletId, this.coin, this.contract}); final String? walletId; final CryptoCurrency? coin; @@ -111,19 +107,19 @@ class _ExchangeFormState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Updating exchange rate", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Updating exchange rate", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -185,27 +181,26 @@ class _ExchangeFormState extends ConsumerState { Future _getAggregateCurrency(Currency currency) async { final rateType = ref.read(efRateTypeProvider); - final currencies = await ExchangeDataLoadingService.instance.isar.currencies - .filter() - .group( - (q) => rateType == ExchangeRateType.fixed - ? q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.fixed) - : q - .rateTypeEqualTo(SupportedRateType.both) - .or() - .rateTypeEqualTo(SupportedRateType.estimated), - ) - .and() - .tickerEqualTo( - currency.ticker, - caseSensitive: false, - ) - .and() - .tokenContractEqualTo(currency.tokenContract) - .findAll(); + final currencies = + await ExchangeDataLoadingService.instance.isar.currencies + .filter() + .group( + (q) => + rateType == ExchangeRateType.fixed + ? q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.fixed) + : q + .rateTypeEqualTo(SupportedRateType.both) + .or() + .rateTypeEqualTo(SupportedRateType.estimated), + ) + .and() + .tickerEqualTo(currency.ticker, caseSensitive: false) + .and() + .tokenContractEqualTo(currency.tokenContract) + .findAll(); final items = [Tuple2(currency.exchangeName, currency)]; @@ -242,10 +237,9 @@ class _ExchangeFormState extends ConsumerState { if (selectedCurrency != null) { await showUpdatingExchangeRate( whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref.read(efCurrencyPairProvider).setSend( - aggregateSelected, - notifyListeners: true, - ), + (aggregateSelected) => ref + .read(efCurrencyPairProvider) + .setSend(aggregateSelected, notifyListeners: true), ), ); } @@ -269,10 +263,9 @@ class _ExchangeFormState extends ConsumerState { if (selectedCurrency != null) { await showUpdatingExchangeRate( whileFuture: _getAggregateCurrency(selectedCurrency).then( - (aggregateSelected) => ref.read(efCurrencyPairProvider).setReceive( - aggregateSelected, - notifyListeners: true, - ), + (aggregateSelected) => ref + .read(efCurrencyPairProvider) + .setReceive(aggregateSelected, notifyListeners: true), ), ); } @@ -284,20 +277,20 @@ class _ExchangeFormState extends ConsumerState { _receiveFocusNode.unfocus(); final temp = ref.read(efCurrencyPairProvider).send; - ref.read(efCurrencyPairProvider).setSend( + ref + .read(efCurrencyPairProvider) + .setSend( ref.read(efCurrencyPairProvider).receive, notifyListeners: true, ); - ref.read(efCurrencyPairProvider).setReceive( - temp, - notifyListeners: true, - ); + ref.read(efCurrencyPairProvider).setReceive(temp, notifyListeners: true); // final reversed = ref.read(efReversedProvider); final amount = ref.read(efSendAmountProvider); - ref.read(efSendAmountProvider.notifier).state = - ref.read(efReceiveAmountProvider); + ref.read(efSendAmountProvider.notifier).state = ref.read( + efReceiveAmountProvider, + ); ref.read(efReceiveAmountProvider.notifier).state = amount; @@ -315,72 +308,73 @@ class _ExchangeFormState extends ConsumerState { _sendFocusNode.unfocus(); _receiveFocusNode.unfocus(); - final result = isDesktop - ? await showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxHeight: 700, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( + final result = + isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Choose a coin to exchange", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( padding: const EdgeInsets.only( left: 32, + right: 32, + bottom: 32, ), - child: Text( - "Choose a coin to exchange", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: Row( - children: [ - Expanded( - child: RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: Theme.of(context) - .extension()! - .background, - child: ExchangeCurrencySelectionView( - willChangeTicker: willChange, - pairedTicker: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: + Theme.of( + context, + ).extension()!.background, + child: ExchangeCurrencySelectionView( + willChangeTicker: willChange, + pairedTicker: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, + ), ), ), - ), - ], + ], + ), ), ), + ], + ), + ); + }, + ) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: + (_) => ExchangeCurrencySelectionView( + willChangeTicker: willChange, + pairedTicker: paired, + isFixedRate: isFixedRate, + willChangeIsSend: willChangeIsSend, ), - ], - ), - ); - }, - ) - : await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ExchangeCurrencySelectionView( - willChangeTicker: willChange, - pairedTicker: paired, - isFixedRate: isFixedRate, - willChangeIsSend: willChangeIsSend, ), - ), - ); + ); if (mounted && result is Currency) { return result; @@ -398,20 +392,31 @@ class _ExchangeFormState extends ConsumerState { } void onExchangePressed() async { + final exchangeName = ref.read(efExchangeProvider).name; + final rateType = ref.read(efRateTypeProvider); final fromTicker = ref.read(efCurrencyPairProvider).send?.ticker ?? ""; + final fromNetwork = ref + .read(efCurrencyPairProvider) + .send + ?.networkFor(exchangeName); final toTicker = ref.read(efCurrencyPairProvider).receive?.ticker ?? ""; + final toNetwork = ref + .read(efCurrencyPairProvider) + .receive + ?.networkFor(exchangeName); final estimate = ref.read(efEstimateProvider)!; final sendAmount = ref.read(efSendAmountProvider)!; if (rateType == ExchangeRateType.fixed && toTicker.toUpperCase() == "WOW") { await showDialog( context: context, - builder: (context) => const StackOkDialog( - title: "WOW error", - message: - "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", - ), + builder: + (context) => const StackOkDialog( + title: "WOW error", + message: + "Wownero is temporarily disabled as a receiving currency for fixed rate trades due to network issues", + ), ); return; @@ -421,9 +426,10 @@ class _ExchangeFormState extends ConsumerState { final amountToSend = estimate.reversed ? estimate.estimatedAmount : sendAmount; - final amountToReceive = estimate.reversed - ? ref.read(efReceiveAmountProvider)! - : estimate.estimatedAmount; + final amountToReceive = + estimate.reversed + ? ref.read(efReceiveAmountProvider)! + : estimate.estimatedAmount; switch (rateType) { case ExchangeRateType.estimated: @@ -466,32 +472,30 @@ class _ExchangeFormState extends ConsumerState { "Do you want to attempt trade anyways?", style: STextStyles.desktopTextSmall(context), ), - const Spacer( - flex: 2, - ), + const Spacer(flex: 2), Row( children: [ Expanded( child: SecondaryButton( label: "Cancel", buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(true), + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(true), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( label: "Attempt", buttonHeight: ButtonHeight.l, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(false), + onPressed: + () => Navigator.of( + context, + rootNavigator: true, + ).pop(false), ), ), ], @@ -521,10 +525,7 @@ class _ExchangeFormState extends ConsumerState { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonStyle(context), - child: Text( - "Attempt", - style: STextStyles.button(context), - ), + child: Text("Attempt", style: STextStyles.button(context)), onPressed: () { // continue and try to attempt trade Navigator.of(context).pop(false); @@ -540,15 +541,15 @@ class _ExchangeFormState extends ConsumerState { return; } rate = - "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal( - scaleOnInfinitePrecision: 12, - ).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; + "1 ${fromTicker.toUpperCase()} ~${(amountToReceive / amountToSend).toDecimal(scaleOnInfinitePrecision: 12).toStringAsFixed(8)} ${toTicker.toUpperCase()}"; break; } final model = IncompleteExchangeModel( sendTicker: fromTicker.toUpperCase(), + sendNetwork: fromNetwork, receiveTicker: toTicker.toUpperCase(), + receiveNetwork: toNetwork, rateInfo: rate, sendAmount: amountToSend, receiveAmount: amountToReceive, @@ -560,8 +561,10 @@ class _ExchangeFormState extends ConsumerState { if (mounted) { if (walletInitiated) { - ref.read(exchangeSendFromWalletIdStateProvider.state).state = - Tuple2(walletId!, coin!); + ref.read(exchangeSendFromWalletIdStateProvider.state).state = Tuple2( + walletId!, + coin!, + ); if (isDesktop) { ref.read(ssss.state).state = model; await showDialog( @@ -571,18 +574,15 @@ class _ExchangeFormState extends ConsumerState { return const DesktopDialog( maxWidth: 720, maxHeight: double.infinity, - child: StepScaffold( - initialStep: 2, - ), + child: StepScaffold(initialStep: 2), ); }, ); } else { unawaited( - Navigator.of(context).pushNamed( - Step2View.routeName, - arguments: model, - ), + Navigator.of( + context, + ).pushNamed(Step2View.routeName, arguments: model), ); } } else { @@ -597,18 +597,15 @@ class _ExchangeFormState extends ConsumerState { return const DesktopDialog( maxWidth: 720, maxHeight: double.infinity, - child: StepScaffold( - initialStep: 1, - ), + child: StepScaffold(initialStep: 1), ); }, ); } else { unawaited( - Navigator.of(context).pushNamed( - Step1View.routeName, - arguments: model, - ), + Navigator.of( + context, + ).pushNamed(Step1View.routeName, arguments: model), ); } } @@ -620,9 +617,10 @@ class _ExchangeFormState extends ConsumerState { return false; } - final String? ticker = isSend - ? ref.read(efCurrencyPairProvider).send?.ticker - : ref.read(efCurrencyPairProvider).receive?.ticker; + final String? ticker = + isSend + ? ref.read(efCurrencyPairProvider).send?.ticker + : ref.read(efCurrencyPairProvider).receive?.ticker; if (ticker == null) { return false; @@ -640,9 +638,10 @@ class _ExchangeFormState extends ConsumerState { } final reversed = ref.read(efReversedProvider); - final amount = reversed - ? ref.read(efReceiveAmountProvider) - : ref.read(efSendAmountProvider); + final amount = + reversed + ? ref.read(efReceiveAmountProvider) + : ref.read(efSendAmountProvider); final pair = ref.read(efCurrencyPairProvider); if (amount == null || @@ -654,7 +653,7 @@ class _ExchangeFormState extends ConsumerState { } final rateType = ref.read(efRateTypeProvider); final Map>, Range?>> - results = {}; + results = {}; for (final exchange in usableExchanges) { final sendCurrency = pair.send?.forExchange(exchange.name); @@ -663,26 +662,29 @@ class _ExchangeFormState extends ConsumerState { if (sendCurrency != null && receiveCurrency != null) { final rangeResponse = await exchange.getRange( reversed ? receiveCurrency.ticker : sendCurrency.ticker, + reversed ? receiveCurrency.network : sendCurrency.network, reversed ? sendCurrency.ticker : receiveCurrency.ticker, + reversed ? sendCurrency.network : receiveCurrency.network, rateType == ExchangeRateType.fixed, ); + Logging.instance.d( + "${exchange.name}: fixedRate=$rateType, RANGE=$rangeResponse", + ); + final estimateResponse = await exchange.getEstimates( sendCurrency.ticker, + sendCurrency.network, receiveCurrency.ticker, + receiveCurrency.network, amount, rateType == ExchangeRateType.fixed, reversed, ); - results.addAll( - { - exchange.name: Tuple2( - estimateResponse, - rangeResponse.value, - ), - }, - ); + results.addAll({ + exchange.name: Tuple2(estimateResponse, rangeResponse.value), + }); } } @@ -743,8 +745,7 @@ class _ExchangeFormState extends ConsumerState { } }); _receiveFocusNode.addListener(() { - if (_receiveFocusNode.hasFocus && - ref.read(efExchangeProvider).name != ChangeNowExchange.exchangeName) { + if (_receiveFocusNode.hasFocus) { final reversed = ref.read(efReversedProvider); WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(efReversedProvider.notifier).state = true; @@ -767,18 +768,17 @@ class _ExchangeFormState extends ConsumerState { .setReceive(null, notifyListeners: true); ExchangeDataLoadingService.instance .getAggregateCurrency( - widget.contract == null ? coin!.ticker : widget.contract!.symbol, - ExchangeRateType.estimated, - widget.contract == null ? null : widget.contract!.address, - ) + widget.contract == null ? coin!.ticker : widget.contract!.symbol, + ExchangeRateType.estimated, + widget.contract?.address, + ) .then((value) { - if (value != null) { - ref.read(efCurrencyPairProvider).setSend( - value, - notifyListeners: true, - ); - } - }); + if (value != null) { + ref + .read(efCurrencyPairProvider) + .setSend(value, notifyListeners: true); + } + }); }); } else { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -816,9 +816,7 @@ class _ExchangeFormState extends ConsumerState { if (_sendFocusNode.hasFocus) { _sendController.selection = TextSelection.fromPosition( - TextPosition( - offset: _sendController.text.length, - ), + TextPosition(offset: _sendController.text.length), ); } } @@ -835,9 +833,7 @@ class _ExchangeFormState extends ConsumerState { if (_receiveFocusNode.hasFocus) { _receiveController.selection = TextSelection.fromPosition( - TextPosition( - offset: _receiveController.text.length, - ), + TextPosition(offset: _receiveController.text.length), ); } } @@ -868,13 +864,13 @@ class _ExchangeFormState extends ConsumerState { color: Theme.of(context).extension()!.textDark3, ), ), - SizedBox( - height: isDesktop ? 10 : 4, - ), + SizedBox(height: isDesktop ? 10 : 4), ExchangeTextField( - key: Key("exchangeTextFieldKeyFor_" - "${Theme.of(context).extension()!.themeId}" - "${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}"), + key: Key( + "exchangeTextFieldKeyFor_" + "${Theme.of(context).extension()!.themeId}" + "${ref.watch(efCurrencyPairProvider.select((value) => value.send?.ticker))}", + ), controller: _sendController, focusNode: _sendFocusNode, textStyle: STextStyles.smallMed14(context).copyWith( @@ -893,15 +889,12 @@ class _ExchangeFormState extends ConsumerState { onChanged: sendFieldOnChanged, onButtonTap: selectSendCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: - ref.watch(efCurrencyPairProvider.select((value) => value.send)), - ), - SizedBox( - height: isDesktop ? 10 : 4, - ), - SizedBox( - height: isDesktop ? 10 : 4, + currency: ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ), ), + SizedBox(height: isDesktop ? 10 : 4), + SizedBox(height: isDesktop ? 10 : 4), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -914,20 +907,23 @@ class _ExchangeFormState extends ConsumerState { ), ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), child: Semantics( label: "Swap Button. Reverse The Exchange Currencies.", excludeSemantics: true, child: RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(6) - : const EdgeInsets.all(2), - color: Theme.of(context) - .extension()! - .buttonBackSecondary, + padding: + isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: + Theme.of( + context, + ).extension()!.buttonBackSecondary, radiusMultiplier: 0.75, child: GestureDetector( onTap: () async { @@ -939,9 +935,10 @@ class _ExchangeFormState extends ConsumerState { Assets.svg.swap, width: 20, height: 20, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), @@ -950,9 +947,7 @@ class _ExchangeFormState extends ConsumerState { ), ], ), - SizedBox( - height: isDesktop ? 10 : 7, - ), + SizedBox(height: isDesktop ? 10 : 7), ExchangeTextField( key: Key( "exchangeTextFieldKeyFor1_${Theme.of(context).extension()!.themeId}", @@ -967,52 +962,42 @@ class _ExchangeFormState extends ConsumerState { borderRadius: Constants.size.circularBorderRadius, background: Theme.of(context).extension()!.textFieldDefaultBG, - onTap: rateType == ExchangeRateType.estimated && - ref.watch(efExchangeProvider).name == - ChangeNowExchange.exchangeName - ? null - : () { - if (_sendController.text == "-") { - _sendController.text = ""; - } - }, + onTap: + rateType == ExchangeRateType.estimated + ? null + : () { + if (_sendController.text == "-") { + _sendController.text = ""; + } + }, onChanged: receiveFieldOnChanged, onButtonTap: selectReceiveCurrency, isWalletCoin: isWalletCoin(coin, true), - currency: ref - .watch(efCurrencyPairProvider.select((value) => value.receive)), - readOnly: rateType == ExchangeRateType.estimated && - ref.watch(efExchangeProvider).name == - ChangeNowExchange.exchangeName, - ), - SizedBox( - height: isDesktop ? 20 : 12, + currency: ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ), + readOnly: rateType == ExchangeRateType.estimated, ), + SizedBox(height: isDesktop ? 20 : 12), SizedBox( height: isDesktop ? 60 : 40, - child: RateTypeToggle( - key: UniqueKey(), - onChanged: onRateTypeChanged, - ), + child: RateTypeToggle(key: UniqueKey(), onChanged: onRateTypeChanged), ), AnimatedSize( duration: const Duration(milliseconds: 300), - child: ref.watch(efSendAmountProvider) == null && - ref.watch(efReceiveAmountProvider) == null - ? const SizedBox( - height: 0, - ) - : Padding( - padding: EdgeInsets.only(top: isDesktop ? 20 : 12), - child: ExchangeProviderOptions( - fixedRate: rateType == ExchangeRateType.fixed, - reversed: ref.watch(efReversedProvider), + child: + ref.watch(efSendAmountProvider) == null && + ref.watch(efReceiveAmountProvider) == null + ? const SizedBox(height: 0) + : Padding( + padding: EdgeInsets.only(top: isDesktop ? 20 : 12), + child: ExchangeProviderOptions( + fixedRate: rateType == ExchangeRateType.fixed, + reversed: ref.watch(efReversedProvider), + ), ), - ), - ), - SizedBox( - height: isDesktop ? 20 : 12, ), + SizedBox(height: isDesktop ? 20 : 12), PrimaryButton( buttonHeight: isDesktop ? ButtonHeight.l : null, enabled: ref.watch(efCanExchangeProvider), diff --git a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart index 2cd014fc3..92c713acb 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_3_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_3_view.dart @@ -79,10 +79,7 @@ class _Step3ViewState extends ConsumerState { } }, ), - title: Text( - "Swap", - style: STextStyles.navBarTitle(context), - ), + title: Text("Swap", style: STextStyles.navBarTitle(context)), ), body: LayoutBuilder( builder: (context, constraints) { @@ -100,21 +97,13 @@ class _Step3ViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - StepRow( - count: 4, - current: 2, - width: width, - ), - const SizedBox( - height: 14, - ), + StepRow(count: 4, current: 2, width: width), + const SizedBox(height: 14), Text( "Confirm exchange details", style: STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), RoundedWhiteContainer( child: Row( children: [ @@ -130,9 +119,7 @@ class _Step3ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( children: [ @@ -148,9 +135,7 @@ class _Step3ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Row( children: [ @@ -166,9 +151,7 @@ class _Step3ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -177,9 +160,7 @@ class _Step3ViewState extends ConsumerState { "Recipient ${model.receiveTicker.toUpperCase()} address", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( model.recipientAddress!, style: STextStyles.itemSubtitle12(context), @@ -187,10 +168,7 @@ class _Step3ViewState extends ConsumerState { ], ), ), - if (supportsRefund) - const SizedBox( - height: 8, - ), + if (supportsRefund) const SizedBox(height: 8), if (supportsRefund) RoundedWhiteContainer( child: Column( @@ -200,9 +178,7 @@ class _Step3ViewState extends ConsumerState { "Refund ${model.sendTicker.toUpperCase()} address", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( model.refundAddress!, style: STextStyles.itemSubtitle12(context), @@ -210,9 +186,7 @@ class _Step3ViewState extends ConsumerState { ], ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), const Spacer(), Row( children: [ @@ -227,16 +201,15 @@ class _Step3ViewState extends ConsumerState { child: Text( "Back", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .buttonTextSecondary, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, ), ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: TextButton( onPressed: () async { @@ -244,19 +217,22 @@ class _Step3ViewState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of(context) + .extension()! + .overlay + .withOpacity(0.6), + child: + const CustomLoadingOverlay( + message: + "Creating a trade", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -265,18 +241,23 @@ class _Step3ViewState extends ConsumerState { .read(efExchangeProvider) .createTrade( from: model.sendTicker, + fromNetwork: model.sendNetwork, to: model.receiveTicker, - fixedRate: model.rateType != + toNetwork: model.receiveNetwork, + fixedRate: + model.rateType != ExchangeRateType.estimated, - amount: model.reversed - ? model.receiveAmount - : model.sendAmount, + amount: + model.reversed + ? model.receiveAmount + : model.sendAmount, addressTo: model.recipientAddress!, extraId: null, - addressRefund: supportsRefund - ? model.refundAddress! - : "", + addressRefund: + supportsRefund + ? model.refundAddress! + : "", refundExtraId: "", estimate: model.estimate, reversed: model.reversed, @@ -304,10 +285,12 @@ class _Step3ViewState extends ConsumerState { showDialog( context: context, barrierDismissible: true, - builder: (_) => StackDialog( - title: "Failed to create trade", - message: message ?? "", - ), + builder: + (_) => StackDialog( + title: + "Failed to create trade", + message: message ?? "", + ), ), ); } @@ -315,7 +298,9 @@ class _Step3ViewState extends ConsumerState { } // save trade to hive - await ref.read(tradesServiceProvider).add( + await ref + .read(tradesServiceProvider) + .add( trade: response.value!, shouldNotifyListeners: true, ); 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/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index 1ffa12275..6bc4b5c04 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -70,10 +70,12 @@ class _ExchangeProviderOptionsState @override Widget build(BuildContext context) { - final sendCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.send)); - final receivingCurrency = - ref.watch(efCurrencyPairProvider.select((value) => value.receive)); + final sendCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.send), + ); + final receivingCurrency = ref.watch( + efCurrencyPairProvider.select((value) => value.receive), + ); final showChangeNow = exchangeSupported( exchangeName: ChangeNowExchange.exchangeName, @@ -98,9 +100,10 @@ class _ExchangeProviderOptionsState return RoundedWhiteContainer( padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12), - borderColor: isDesktop - ? Theme.of(context).extension()!.background - : null, + borderColor: + isDesktop + ? Theme.of(context).extension()!.background + : null, child: Column( children: [ if (showChangeNow) @@ -112,13 +115,10 @@ class _ExchangeProviderOptionsState if (showChangeNow && showMajesticBank) isDesktop ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), + height: 1, + color: Theme.of(context).extension()!.background, + ) + : const SizedBox(height: 16), if (showMajesticBank) ExchangeOption( exchange: MajesticBankExchange.instance, @@ -128,13 +128,10 @@ class _ExchangeProviderOptionsState if ((showChangeNow || showMajesticBank) && showTrocador) isDesktop ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), + height: 1, + color: Theme.of(context).extension()!.background, + ) + : const SizedBox(height: 16), if (showTrocador) ExchangeOption( fixedRate: widget.fixedRate, @@ -145,13 +142,10 @@ class _ExchangeProviderOptionsState showNanswap) isDesktop ? Container( - height: 1, - color: - Theme.of(context).extension()!.background, - ) - : const SizedBox( - height: 16, - ), + height: 1, + color: Theme.of(context).extension()!.background, + ) + : const SizedBox(height: 16), if (showNanswap) ExchangeOption( fixedRate: widget.fixedRate, diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 0416056a7..8bb6017c2 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,7 +20,7 @@ import 'package:tuple/tuple.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../app_config.dart'; -import '../../models/exchange/change_now/exchange_transaction_status.dart'; +import '../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../models/isar/models/blockchain_data/transaction.dart'; import '../../models/isar/stack_theme.dart'; import '../../notifications/show_flush_bar.dart'; @@ -164,7 +164,8 @@ class _TradeDetailsViewState extends ConsumerState { ), ); - final bool hasTx = sentFromStack || + final bool hasTx = + sentFromStack || !(trade.status == "New" || trade.status == "new" || trade.status == "Waiting" || @@ -176,7 +177,8 @@ class _TradeDetailsViewState extends ConsumerState { trade.status == "Expired" || trade.status == "expired" || trade.status == "Failed" || - trade.status == "failed"); + trade.status == "failed" || + trade.status.toLowerCase().startsWith("waiting")); //todo: check if print needed // debugPrint("walletId: $walletId"); @@ -190,9 +192,14 @@ class _TradeDetailsViewState extends ConsumerState { final isDesktop = Util.isDesktop; - final showSendFromStackButton = !hasTx && - !["xmr", "monero", "wow", "wownero"] - .contains(trade.payInCurrency.toLowerCase()) && + final showSendFromStackButton = + !hasTx && + ![ + "xmr", + "monero", + "wow", + "wownero", + ].contains(trade.payInCurrency.toLowerCase()) && AppConfig.isStackCoin(trade.payInCurrency) && (trade.status == "New" || trade.status == "new" || @@ -201,132 +208,128 @@ class _TradeDetailsViewState extends ConsumerState { 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 { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Trade details", - style: STextStyles.navBarTitle(context), - ), - ), - body: Padding( - padding: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Trade details", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), ), ), ), - ), - ), child: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(0), child: BranchedParent( condition: isDesktop, - conditionBranchBuilder: (children) => Padding( - padding: const EdgeInsets.only( - right: 20, - ), - child: Padding( - padding: const EdgeInsets.only( - right: 12, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RoundedWhiteContainer( - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - padding: const EdgeInsets.all(0), - child: ListView( - primary: false, - shrinkWrap: true, - children: children, - ), - ), - if (showSendFromStackButton) - const SizedBox( - height: 32, - ), - if (showSendFromStackButton) - SecondaryButton( - label: "Send from ${AppConfig.prefix}", - buttonHeight: ButtonHeight.l, - onPressed: () { - CryptoCurrency coin; - try { - coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; - } catch (_) { - coin = AppConfig.getCryptoCurrencyByPrettyName( - trade.payInCurrency, - ); - } - final amount = Amount.fromDecimal( - sendAmount, - fractionDigits: coin.fractionDigits, - ); - final address = trade.payInAddress; + conditionBranchBuilder: + (children) => Padding( + padding: const EdgeInsets.only(right: 20), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + RoundedWhiteContainer( + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + padding: const EdgeInsets.all(0), + child: ListView( + primary: false, + shrinkWrap: true, + children: children, + ), + ), + if (showSendFromStackButton) const SizedBox(height: 32), + if (showSendFromStackButton) + SecondaryButton( + label: "Send from ${AppConfig.prefix}", + buttonHeight: ButtonHeight.l, + onPressed: () { + CryptoCurrency coin; + try { + coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; + } catch (_) { + coin = AppConfig.getCryptoCurrencyByPrettyName( + trade.payInCurrency, + ); + } + final amount = Amount.fromDecimal( + sendAmount, + fractionDigits: coin.fractionDigits, + ); + final address = trade.payInAddress; - Navigator.of(context).pushNamed( - SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, - ), - ); - }, - ), - const SizedBox( - height: 32, + Navigator.of(context).pushNamed( + SendFromView.routeName, + arguments: Tuple4(coin, amount, address, trade), + ); + }, + ), + const SizedBox(height: 32), + ], ), - ], + ), + ), + otherBranchBuilder: + (children) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: children, ), - ), - ), - otherBranchBuilder: (children) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, - children: children, - ), children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), ), - ), - ) - : null, + ) + : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -343,9 +346,7 @@ class _TradeDetailsViewState extends ConsumerState { width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SelectableText( "Swap service", style: STextStyles.desktopTextMedium(context), @@ -353,25 +354,24 @@ class _TradeDetailsViewState extends ConsumerState { ], ), Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", style: STextStyles.titleBold12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Builder( builder: (context) { String text; try { final coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + trade.payInCurrency, + )!; final amount = sendAmount.toAmount( fractionDigits: coin.fractionDigits, ); @@ -419,15 +419,12 @@ class _TradeDetailsViewState extends ConsumerState { ), ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -435,10 +432,7 @@ class _TradeDetailsViewState extends ConsumerState { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Status", - style: STextStyles.itemSubtitle(context), - ), + Text("Status", style: STextStyles.itemSubtitle(context)), if (trade.exchangeName == MajesticBankExchange.exchangeName && trade.status == "Completed") @@ -449,137 +443,136 @@ class _TradeDetailsViewState extends ConsumerState { onTap: () { showDialog( context: context, - builder: (context) => const StackOkDialog( - title: "Trade Info", - message: - "Majestic Bank does not store order data indefinitely", - ), + builder: + (context) => const StackOkDialog( + title: "Trade Info", + message: + "Majestic Bank does not store order data indefinitely", + ), ); }, child: SvgPicture.asset( Assets.svg.circleInfo, height: 20, width: 20, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), ), ], ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( trade.status, style: STextStyles.itemSubtitle(context).copyWith( - color: Theme.of(context) - .extension()! - .colorForStatus(trade.status), + color: Theme.of( + context, + ).extension()!.colorForStatus(trade.status), ), ), ], ), ), if (!sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - color: isDesktop - ? Theme.of(context).extension()!.popupBG - : Theme.of(context) - .extension()! - .warningBackground, + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: + isDesktop + ? Theme.of(context).extension()!.popupBG + : Theme.of( + context, + ).extension()!.warningBackground, child: ConditionalParent( condition: isDesktop, - builder: (child) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + builder: + (child) => Column( + mainAxisSize: MainAxisSize.min, children: [ - Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Amount", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ), - ), - const SizedBox( - height: 2, - ), - Text( - "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Amount", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + Text( + "${trade.payInAmount} ${trade.payInCurrency.toUpperCase()}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), + ), + ], ), + IconCopyButton(data: trade.payInAmount), ], ), - IconCopyButton( - data: trade.payInAmount, - ), + const SizedBox(height: 6), + child, ], ), - const SizedBox( - height: 6, - ), - child, - ], - ), child: RichText( text: TextSpan( text: - "You must send at least ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}. ", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, - ), + "You must send at least ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}. ", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, + ), children: [ TextSpan( text: - "If you send less than ${sendAmount.toStringAsFixed( - trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8, - )} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .accentColorRed, - ) - : STextStyles.label(context).copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, - ), + "If you send less than ${sendAmount.toStringAsFixed(trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}, your transaction may not be converted and it may not be refunded.", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorRed, + ) + : STextStyles.label(context).copyWith( + color: + Theme.of(context) + .extension()! + .warningForeground, + ), ), ], ), @@ -587,39 +580,30 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Sent from", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), + Text("Sent from", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 4), SelectableText( widget.walletName!, style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), CustomTextButton( text: "View transaction", onTap: () { - final coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; if (isDesktop) { Navigator.of(context).push( @@ -655,16 +639,13 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (sentFromStack) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -677,9 +658,7 @@ class _TradeDetailsViewState extends ConsumerState { "${trade.exchangeName} address", style: STextStyles.itemSubtitle(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( children: [ Flexible( @@ -693,24 +672,18 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) - IconCopyButton( - data: trade.payInAddress, - ), + if (isDesktop) IconCopyButton(data: trade.payInAddress), ], ), ), if (!sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (!sentFromStack && !hasTx) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -722,52 +695,45 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.itemSubtitle(context), ), isDesktop - ? IconCopyButton( - data: trade.payInAddress, - ) + ? IconCopyButton(data: trade.payInAddress) : GestureDetector( - onTap: () async { - final address = trade.payInAddress; - await Clipboard.setData( - ClipboardData( - text: address, + onTap: () async { + final address = trade.payInAddress; + await Clipboard.setData( + ClipboardData(text: address), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, ), ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], ), + ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( children: [ Expanded( @@ -778,9 +744,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), GestureDetector( onTap: () { showDialog( @@ -799,9 +763,7 @@ class _TradeDetailsViewState extends ConsumerState { style: STextStyles.pageTitleH2(context), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: RepaintBoundary( // key: _qrKey, @@ -815,9 +777,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Center( child: SizedBox( width: width, @@ -833,11 +793,13 @@ class _TradeDetailsViewState extends ConsumerState { ), child: Text( "Cancel", - style: STextStyles.button(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + style: STextStyles.button( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -855,13 +817,12 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.qrcode, width: 12, height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), + const SizedBox(width: 4), Text( "Show QR code", style: STextStyles.link2(context), @@ -872,73 +833,60 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Memo", - style: STextStyles.itemSubtitle(context), - ), + Text("Memo", style: STextStyles.itemSubtitle(context)), isDesktop - ? IconCopyButton( - data: trade.payInExtraId, - ) + ? IconCopyButton(data: trade.payInExtraId) : GestureDetector( - onTap: () async { - final address = trade.payInExtraId; - await Clipboard.setData( - ClipboardData( - text: address, + onTap: () async { + final address = trade.payInExtraId; + await Clipboard.setData( + ClipboardData(text: address), + ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, ), ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); - } - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.copy, - width: 12, - height: 12, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), - ), - ], - ), + } + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.copy, + width: 12, + height: 12, + color: + Theme.of(context) + .extension()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Copy", + style: STextStyles.link2(context), + ), + ], ), + ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( trade.payInExtraId, style: STextStyles.itemSubtitle12(context), @@ -947,15 +895,12 @@ class _TradeDetailsViewState extends ConsumerState { ), ), if (trade.payInExtraId.isNotEmpty && !sentFromStack && !hasTx) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -968,6 +913,86 @@ class _TradeDetailsViewState extends ConsumerState { ), isDesktop ? IconPencilButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditTradeNoteView( + tradeId: tradeId, + note: ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), + ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditTradeNoteView.routeName, + arguments: Tuple2( + tradeId, + ref + .read(tradeNoteServiceProvider) + .getNote(tradeId: tradeId), + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, + ), + const SizedBox(width: 4), + Text("Edit", style: STextStyles.link2(context)), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + SelectableText( + ref.watch( + tradeNoteServiceProvider.select( + (value) => value.getNote(tradeId: tradeId), + ), + ), + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (sentFromStack) + isDesktop ? const _Divider() : const SizedBox(height: 12), + if (sentFromStack) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction note", + style: STextStyles.itemSubtitle(context), + ), + isDesktop + ? IconPencilButton( onPressed: () { showDialog( context: context, @@ -975,26 +1000,22 @@ class _TradeDetailsViewState extends ConsumerState { return DesktopDialog( maxWidth: 580, maxHeight: 360, - child: EditTradeNoteView( - tradeId: tradeId, - note: ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), + child: EditNoteView( + txid: transactionIfSentFromStack!.txid, + walletId: walletId!, ), ); }, ); }, ) - : GestureDetector( + : GestureDetector( onTap: () { Navigator.of(context).pushNamed( - EditTradeNoteView.routeName, + EditNoteView.routeName, arguments: Tuple2( - tradeId, - ref - .read(tradeNoteServiceProvider) - .getNote(tradeId: tradeId), + transactionIfSentFromStack!.txid, + walletId, ), ); }, @@ -1004,13 +1025,12 @@ class _TradeDetailsViewState extends ConsumerState { Assets.svg.pencil, width: 10, height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, + color: + Theme.of(context) + .extension()! + .infoItemIcons, ), + const SizedBox(width: 4), Text( "Edit", style: STextStyles.link2(context), @@ -1018,105 +1038,16 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - ], - ), - const SizedBox( - height: 4, - ), - SelectableText( - ref.watch( - tradeNoteServiceProvider - .select((value) => value.getNote(tradeId: tradeId)), - ), - style: STextStyles.itemSubtitle12(context), - ), - ], - ), - ), - if (sentFromStack) - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), - if (sentFromStack) - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Transaction note", - style: STextStyles.itemSubtitle(context), - ), - isDesktop - ? IconPencilButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: - transactionIfSentFromStack!.txid, - walletId: walletId!, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple2( - transactionIfSentFromStack!.txid, - walletId, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2(context), - ), - ], - ), - ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( ref .watch( - pTransactionNote( - ( - txid: transactionIfSentFromStack!.txid, - walletId: walletId!, - ), - ), + pTransactionNote(( + txid: transactionIfSentFromStack!.txid, + walletId: walletId!, + )), ) ?.value ?? "", @@ -1125,15 +1056,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1142,24 +1070,20 @@ class _TradeDetailsViewState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Date", - style: STextStyles.itemSubtitle(context), - ), - if (isDesktop) - const SizedBox( - height: 2, - ), + Text("Date", style: STextStyles.itemSubtitle(context)), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( Format.extractDateFrom( trade.timestamp.millisecondsSinceEpoch ~/ 1000, ), - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -1180,15 +1104,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1200,10 +1121,7 @@ class _TradeDetailsViewState extends ConsumerState { "Swap service", style: STextStyles.itemSubtitle(context), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( trade.exchangeName, @@ -1211,10 +1129,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) - IconCopyButton( - data: trade.exchangeName, - ), + if (isDesktop) IconCopyButton(data: trade.exchangeName), if (!isDesktop) SelectableText( trade.exchangeName, @@ -1223,15 +1138,12 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, @@ -1243,10 +1155,7 @@ class _TradeDetailsViewState extends ConsumerState { "Trade ID", style: STextStyles.itemSubtitle(context), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) Text( trade.tradeId, @@ -1254,10 +1163,7 @@ class _TradeDetailsViewState extends ConsumerState { ), ], ), - if (isDesktop) - IconCopyButton( - data: trade.tradeId, - ), + if (isDesktop) IconCopyButton(data: trade.tradeId), if (!isDesktop) Row( children: [ @@ -1265,9 +1171,7 @@ class _TradeDetailsViewState extends ConsumerState { trade.tradeId, style: STextStyles.itemSubtitle12(context), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), GestureDetector( onTap: () async { final data = ClipboardData(text: trade.tradeId); @@ -1284,9 +1188,10 @@ class _TradeDetailsViewState extends ConsumerState { }, child: SvgPicture.asset( Assets.svg.copy, - color: Theme.of(context) - .extension()! - .infoItemIcons, + color: + Theme.of( + context, + ).extension()!.infoItemIcons, width: 12, ), ), @@ -1295,25 +1200,17 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - isDesktop - ? const _Divider() - : const SizedBox( - height: 12, - ), + isDesktop ? const _Divider() : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Tracking", - style: STextStyles.itemSubtitle(context), - ), - const SizedBox( - height: 4, - ), + Text("Tracking", style: STextStyles.itemSubtitle(context)), + const SizedBox(height: 4), Builder( builder: (context) { late final String url; @@ -1336,18 +1233,20 @@ class _TradeDetailsViewState extends ConsumerState { break; default: - if (trade.exchangeName - .startsWith(TrocadorExchange.exchangeName)) { + if (trade.exchangeName.startsWith( + TrocadorExchange.exchangeName, + )) { url = "https://trocador.app/en/checkout/${trade.tradeId}"; } } return ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), child: GestureDetector( onTap: () { launchUrl( @@ -1355,10 +1254,7 @@ class _TradeDetailsViewState extends ConsumerState { mode: LaunchMode.externalApplication, ); }, - child: Text( - url, - style: STextStyles.link2(context), - ), + child: Text(url, style: STextStyles.link2(context)), ), ); }, @@ -1366,19 +1262,17 @@ class _TradeDetailsViewState extends ConsumerState { ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop && showSendFromStackButton) SecondaryButton( label: "Send from ${AppConfig.prefix}", onPressed: () { CryptoCurrency coin; try { - coin = AppConfig.getCryptoCurrencyForTicker( - trade.payInCurrency, - )!; + coin = + AppConfig.getCryptoCurrencyForTicker( + trade.payInCurrency, + )!; } catch (_) { coin = AppConfig.getCryptoCurrencyByPrettyName( trade.payInCurrency, @@ -1392,12 +1286,7 @@ class _TradeDetailsViewState extends ConsumerState { Navigator.of(context).pushNamed( SendFromView.routeName, - arguments: Tuple4( - coin, - amount, - address, - trade, - ), + arguments: Tuple4(coin, amount, address, trade), ); }, ), diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index ec0fc926a..11dbb5c16 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -33,6 +33,7 @@ import '../../utilities/constants.dart'; import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; +import '../../wallets/crypto_currency/coins/ethereum.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; @@ -96,11 +97,7 @@ class _ConfirmNameTransactionViewState ), ); - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); final List txids = []; Future txDataFuture; @@ -111,10 +108,7 @@ class _ConfirmNameTransactionViewState txDataFuture = wallet.confirmSend(txData: widget.txData); // await futures in parallel - final futureResults = await Future.wait([ - txDataFuture, - time, - ]); + final futureResults = await Future.wait([txDataFuture, time]); final txData = (futureResults.first as TxData); @@ -126,7 +120,9 @@ class _ConfirmNameTransactionViewState Future.delayed(const Duration(seconds: 5)), // associated name data for reg tx - ref.read(secureStoreProvider).write( + ref + .read(secureStoreProvider) + .write( key: nameSaltKeyBuilder( txData.txid!, walletId, @@ -141,16 +137,16 @@ class _ConfirmNameTransactionViewState ]); txids.add(txData.txid!); - ref.refresh(desktopUseUTXOs); + if (coin is! Ethereum) { + ref.refresh(desktopUseUTXOs); + } // save note for (final txid in txids) { - await ref.read(mainDBProvider).putTransactionNote( - TransactionNote( - walletId: walletId, - txid: txid, - value: note, - ), + await ref + .read(mainDBProvider) + .putTransactionNote( + TransactionNote(walletId: walletId, txid: txid, value: note), ); } @@ -192,13 +188,8 @@ class _ConfirmNameTransactionViewState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - niceError, - style: STextStyles.desktopH3(context), - ), - const SizedBox( - height: 24, - ), + Text(niceError, style: STextStyles.desktopH3(context)), + const SizedBox(height: 24), Flexible( child: SingleChildScrollView( child: SelectableText( @@ -207,9 +198,7 @@ class _ConfirmNameTransactionViewState ), ), ), - const SizedBox( - height: 56, - ), + const SizedBox(height: 56), Row( children: [ const Spacer(), @@ -237,9 +226,10 @@ class _ConfirmNameTransactionViewState child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -284,82 +274,79 @@ class _ConfirmNameTransactionViewState 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, + 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), ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 24, + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, ), - child: IntrinsicHeight( - child: Padding( - padding: const EdgeInsets.all(4), - child: child, + 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( + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, children: [ - AppBarBackButton( - size: 40, - iconSize: 24, - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(), - ), - Text( - "Confirm transaction", - style: STextStyles.desktopH3(context), + 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)), ], ), - Flexible( - child: SingleChildScrollView( - child: child, - ), - ), - ], - ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, @@ -372,20 +359,13 @@ class _ConfirmNameTransactionViewState "Confirm Name transaction", style: STextStyles.pageTitleH1(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Name", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Name", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.opNameState!.name, style: STextStyles.itemSubtitle12(context), @@ -393,20 +373,13 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text( - "Value", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), + Text("Value", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), Text( widget.txData.opNameState!.value, style: STextStyles.itemSubtitle12(context), @@ -414,9 +387,7 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -425,9 +396,7 @@ class _ConfirmNameTransactionViewState "Recipient", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Text( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle12(context), @@ -435,30 +404,23 @@ class _ConfirmNameTransactionViewState ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Amount", - style: STextStyles.smallMed12(context), - ), + Text("Amount", style: STextStyles.smallMed12(context)), SelectableText( - ref.watch(pAmountFormatter(coin)).format( - amountWithoutChange, - ), + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), ], ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -476,9 +438,7 @@ class _ConfirmNameTransactionViewState ), ), if (widget.txData.fee != null && widget.txData.vSize != null) - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (widget.txData.fee != null && widget.txData.vSize != null) RoundedWhiteContainer( child: Row( @@ -488,9 +448,7 @@ class _ConfirmNameTransactionViewState "sats/vByte", style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SelectableText( "~${fee.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle12(context), @@ -500,22 +458,15 @@ class _ConfirmNameTransactionViewState ), if (widget.txData.note != null && widget.txData.note!.isNotEmpty) - const SizedBox( - height: 12, - ), + 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, - ), + Text("Note", style: STextStyles.smallMed12(context)), + const SizedBox(height: 4), SelectableText( widget.txData.note!, style: STextStyles.itemSubtitle12(context), @@ -543,9 +494,10 @@ class _ConfirmNameTransactionViewState children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, borderRadius: BorderRadius.only( topLeft: Radius.circular( Constants.size.circularBorderRadius, @@ -573,9 +525,7 @@ class _ConfirmNameTransactionViewState width: 32, height: 32, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Text( "Send $unit Name transaction", style: STextStyles.desktopTextMedium(context), @@ -596,17 +546,16 @@ class _ConfirmNameTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.name, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -614,9 +563,10 @@ class _ConfirmNameTransactionViewState ), Container( height: 1, - color: Theme.of(context) - .extension()! - .background, + color: + Theme.of( + context, + ).extension()!.background, ), Padding( padding: const EdgeInsets.all(12), @@ -630,17 +580,16 @@ class _ConfirmNameTransactionViewState context, ), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), SelectableText( widget.txData.opNameState!.value, style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ], @@ -652,27 +601,24 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - ), + 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, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -684,11 +630,13 @@ class _ConfirmNameTransactionViewState enableSuggestions: isDesktop ? false : true, controller: noteController, focusNode: _noteFocusNode, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), onChanged: (_) => setState(() {}), @@ -704,41 +652,37 @@ class _ConfirmNameTransactionViewState 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 = "", - ); - }, - ), - ], + 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, + ) + : null, ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), ], ), ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Amount", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -746,19 +690,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + 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, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Builder( builder: (context) { final externalCalls = ref.watch( @@ -769,21 +710,21 @@ class _ConfirmNameTransactionViewState String fiatAmount = "N/A"; if (externalCalls) { - final price = ref - .read( - priceAnd24hChangeNotifierProvider, - ) - .getPrice(coin) - .item1; - if (price > Decimal.zero) { + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getPrice(coin) + ?.value; + if (price != null && price > Decimal.zero) { fiatAmount = (amountWithoutChange.decimal * price) .toAmount(fractionDigits: 2) .fiatString( - locale: ref - .read( - localeServiceChangeNotifierProvider, - ) - .locale, + locale: + ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, ); } } @@ -791,30 +732,20 @@ class _ConfirmNameTransactionViewState return Row( children: [ SelectableText( - ref.watch(pAmountFormatter(coin)).format( - amountWithoutChange, - ), - style: STextStyles.itemSubtitle( - context, - ), + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange), + style: STextStyles.itemSubtitle(context), ), if (externalCalls) Text( " | ", - style: STextStyles.itemSubtitle( - context, - ), + style: STextStyles.itemSubtitle(context), ), if (externalCalls) SelectableText( - "~$fiatAmount ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: STextStyles.itemSubtitle( - context, - ), + "~$fiatAmount ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle(context), ), ], ); @@ -824,10 +755,7 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Recipient", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -835,19 +763,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + 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, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( widget.txData.recipients!.first.address, style: STextStyles.itemSubtitle(context), @@ -857,10 +782,7 @@ class _ConfirmNameTransactionViewState // todo amoutn here if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "Transaction fee", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -868,19 +790,16 @@ class _ConfirmNameTransactionViewState ), if (isDesktop) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + 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, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( ref.watch(pAmountFormatter(coin)).format(fee!), style: STextStyles.itemSubtitle(context), @@ -891,10 +810,7 @@ class _ConfirmNameTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - ), + padding: const EdgeInsets.only(top: 16, left: 32), child: Text( "sats/vByte", style: STextStyles.desktopTextExtraExtraSmall(context), @@ -904,19 +820,16 @@ class _ConfirmNameTransactionViewState widget.txData.fee != null && widget.txData.vSize != null) Padding( - padding: const EdgeInsets.only( - top: 10, - left: 32, - right: 32, - ), + 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, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: SelectableText( "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", style: STextStyles.itemSubtitle(context), @@ -924,74 +837,78 @@ class _ConfirmNameTransactionViewState ), ), if (!isDesktop) const Spacer(), - SizedBox( - height: isDesktop ? 23 : 12, - ), + SizedBox(height: isDesktop ? 23 : 12), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + 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, + 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, - ), + 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, - ), + 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, - ), + SizedBox(height: isDesktop ? 28 : 16), Padding( - padding: isDesktop - ? const EdgeInsets.symmetric( - horizontal: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 32) + : const EdgeInsets.all(0), child: PrimaryButton( label: "Send", buttonHeight: isDesktop ? ButtonHeight.l : null, @@ -1001,31 +918,28 @@ class _ConfirmNameTransactionViewState 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, + builder: + (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - DesktopDialogCloseButton(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [DesktopDialogCloseButton()], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend(coin: coin), + ), ], ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: DesktopAuthSend( - coin: coin, - ), - ), - ], - ), - ), + ), ); } else { unlocked = await Navigator.push( @@ -1033,18 +947,21 @@ class _ConfirmNameTransactionViewState 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", + 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", ), - settings: - const RouteSettings(name: "/confirmsendlockscreen"), ), ); } @@ -1057,9 +974,10 @@ class _ConfirmNameTransactionViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: Util.isDesktop - ? "Invalid passphrase" - : "Invalid PIN", + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", context: context, ), ); @@ -1069,10 +987,7 @@ class _ConfirmNameTransactionViewState }, ), ), - if (isDesktop) - const SizedBox( - height: 32, - ), + if (isDesktop) const SizedBox(height: 32), ], ), ), diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index 6b08b84db..3185144f2 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -62,12 +62,11 @@ class _PaynymDetailsPopupState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => WillPopScope( - onWillPop: () async => canPop, - child: const LoadingIndicator( - width: 200, - ), - ), + builder: + (context) => WillPopScope( + onWillPop: () async => canPop, + child: const LoadingIndicator(width: 200), + ), ), ); @@ -111,45 +110,47 @@ class _PaynymDetailsPopupState extends ConsumerState { // show info pop up await showDialog( context: context, - builder: (context) => ConfirmPaynymConnectDialog( - nymName: widget.accountLite.nymName, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - onConfirmPressed: () { - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showDialog( - context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - walletId: widget.walletId, - isPaynymNotificationTransaction: true, - txData: preparedTx, - onSuccess: () { - // do nothing extra - }, - onSuccessInsteadOfRouteOnSuccess: () { - Navigator.of(context, rootNavigator: true).pop(); - Navigator.of(context, rootNavigator: true).pop(); - unawaited( - showFloatingFlushBar( - type: FlushBarType.success, - message: - "Connection initiated to ${widget.accountLite.nymName}", - iconAsset: Assets.svg.copy, - context: context, + builder: + (context) => ConfirmPaynymConnectDialog( + nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + onConfirmPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showDialog( + context: context, + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + walletId: widget.walletId, + isPaynymNotificationTransaction: true, + txData: preparedTx, + onSuccess: () { + // do nothing extra + }, + onSuccessInsteadOfRouteOnSuccess: () { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of(context, rootNavigator: true).pop(); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: + "Connection initiated to ${widget.accountLite.nymName}", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + }, + ), ), - ); - }, ), - ), - ), - ); - }, - amount: preparedTx.amount! + preparedTx.fee!, - coin: ref.read(pWalletCoin(widget.walletId)), - ), + ); + }, + amount: preparedTx.amount! + preparedTx.fee!, + coin: ref.read(pWalletCoin(widget.walletId)), + ), ); } } @@ -157,10 +158,11 @@ class _PaynymDetailsPopupState extends ConsumerState { Future _onSend() async { await showDialog( context: context, - builder: (context) => DesktopPaynymSendDialog( - walletId: widget.walletId, - accountLite: widget.accountLite, - ), + builder: + (context) => DesktopPaynymSendDialog( + walletId: widget.walletId, + accountLite: widget.accountLite, + ), ); } @@ -185,9 +187,7 @@ class _PaynymDetailsPopupState extends ConsumerState { paymentCodeString: widget.accountLite.code, size: 36, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -196,8 +196,9 @@ class _PaynymDetailsPopupState extends ConsumerState { style: STextStyles.desktopTextSmall(context), ), FutureBuilder( - future: paynymWallet - .hasConnected(widget.accountLite.code), + future: paynymWallet.hasConnected( + widget.accountLite.code, + ), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -205,16 +206,16 @@ class _PaynymDetailsPopupState extends ConsumerState { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( "Connected", - style: STextStyles.desktopTextSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorGreen, + style: STextStyles.desktopTextSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorGreen, ), ), ], @@ -228,15 +229,14 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ], ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Row( children: [ Expanded( child: FutureBuilder( - future: - paynymWallet.hasConnected(widget.accountLite.code), + future: paynymWallet.hasConnected( + widget.accountLite.code, + ), builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -249,9 +249,10 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circleArrowUpRight, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: + Theme.of(context) + .extension()! + .buttonTextPrimary, ), iconSpacing: 6, onPressed: _onSend, @@ -264,9 +265,10 @@ class _PaynymDetailsPopupState extends ConsumerState { Assets.svg.circlePlusFilled, width: 16, height: 16, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, + color: + Theme.of(context) + .extension()! + .buttonTextPrimary, ), iconSpacing: 6, onPressed: _onConnectPressed, @@ -281,44 +283,41 @@ class _PaynymDetailsPopupState extends ConsumerState { }, ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), kDisableFollowing ? const Spacer() : Expanded( - child: PaynymFollowToggleButton( - walletId: widget.walletId, - paymentCodeStringToFollow: - widget.accountLite.code, - style: - PaynymFollowToggleButtonStyle.detailsDesktop, - ), + child: PaynymFollowToggleButton( + walletId: widget.walletId, + paymentCodeStringToFollow: widget.accountLite.code, + style: PaynymFollowToggleButtonStyle.detailsDesktop, ), + ), ], ), if (_showInsufficientFundsInfo) Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - height: 24, - ), + const SizedBox(height: 24), RoundedContainer( - color: Theme.of(context) - .extension()! - .warningBackground, + color: + Theme.of( + context, + ).extension()!.warningBackground, child: Text( "Adding a PayNym to your contacts requires a one-time " "transaction fee for creating the record on the " "blockchain. Please deposit more " "${ref.watch(pWalletCoin(widget.walletId)).ticker} " "into your wallet and try again.", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .warningForeground, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.warningForeground, ), ), ), @@ -341,9 +340,7 @@ class _PaynymDetailsPopupState extends ConsumerState { "PayNym address", style: STextStyles.desktopTextExtraExtraSmall(context), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -351,18 +348,18 @@ class _PaynymDetailsPopupState extends ConsumerState { constraints: const BoxConstraints(minHeight: 100), child: Text( widget.accountLite.code, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), QR( padding: const EdgeInsets.all(0), size: 100, @@ -370,16 +367,12 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), CustomTextButton( text: "Copy", onTap: () async { await Clipboard.setData( - ClipboardData( - text: widget.accountLite.code, - ), + ClipboardData(text: widget.accountLite.code), ); unawaited( showFloatingFlushBar( diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 6d40734b0..57a6549ca 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -21,7 +21,6 @@ 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/db/main_db_provider.dart'; import '../../providers/providers.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../route_generator.dart'; @@ -30,9 +29,11 @@ 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/crypto_currency/coins/epiccash.dart'; +import '../../wallets/crypto_currency/coins/ethereum.dart'; import '../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -181,7 +182,9 @@ class _ConfirmTransactionViewState } else { txids.add((results.first as TxData).txid!); } - ref.refresh(desktopUseUTXOs); + if (coin is! Ethereum) { + ref.refresh(desktopUseUTXOs); + } // save note for (final txid in txids) { @@ -201,7 +204,7 @@ class _ConfirmTransactionViewState widget.onSuccess.call(); // pop back to wallet - if (mounted) { + if (context.mounted) { if (widget.onSuccessInsteadOfRouteOnSuccess == null) { Navigator.of( context, @@ -211,7 +214,7 @@ class _ConfirmTransactionViewState } } } on BadEpicHttpAddressException catch (_) { - if (mounted) { + if (context.mounted) { // pop building dialog Navigator.of(context).pop(); unawaited( @@ -225,80 +228,79 @@ class _ConfirmTransactionViewState return; } } catch (e, s) { - //todo: comeback to this - debugPrint("$e\n$s"); + const message = "Broadcast transaction failed"; + Logging.instance.e(message, error: e, stackTrace: s); // pop sending dialog - Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).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( - "Broadcast transaction failed", - style: STextStyles.desktopH3(context), - ), - const SizedBox(height: 24), - Flexible( - child: SingleChildScrollView( - child: SelectableText( - e.toString(), - style: STextStyles.smallMed14(context), + 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(message, 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, + 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: "Broadcast transaction failed", - 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, + ); + } else { + return StackDialog( + title: message, + 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(); + }, ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ); - } - }, - ); + ); + } + }, + ); + } } } @@ -533,6 +535,21 @@ class _ConfirmTransactionViewState ], ), ), + if (coin is Ethereum) const SizedBox(height: 12), + if (coin is Ethereum) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Nonce", style: STextStyles.smallMed12(context)), + SelectableText( + widget.txData.nonce.toString(), + 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) @@ -685,14 +702,14 @@ class _ConfirmTransactionViewState .tokenContract .address, ) - .item1 + ?.value : ref .read( priceAnd24hChangeNotifierProvider, ) .getPrice(coin) - .item1; - if (price > Decimal.zero) { + ?.value; + if (price != null && price > Decimal.zero) { fiatAmount = (amountWithoutChange.decimal * price) .toAmount(fractionDigits: 2) @@ -836,6 +853,42 @@ class _ConfirmTransactionViewState ], ), ), + if (coin is Ethereum) + Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.background, + ), + if (coin is Ethereum) + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nonce", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox(height: 2), + SelectableText( + widget.txData.nonce.toString(), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ), + ), + ], + ), + ), // Container( // height: 1, // color: Theme.of(context) @@ -1211,7 +1264,10 @@ class _ConfirmTransactionViewState unawaited( showFloatingFlushBar( type: FlushBarType.warning, - message: "Invalid PIN", + message: + Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", context: context, ), ); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 4bdcef6e2..3c907605e 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'; @@ -39,6 +40,7 @@ import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/eth_commons.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; @@ -57,6 +59,7 @@ import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/dialogs/firo_exchange_address_dialog.dart'; +import '../../widgets/eth_fee_form.dart'; import '../../widgets/fee_slider.dart'; import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; @@ -98,6 +101,13 @@ class SendView extends ConsumerStatefulWidget { } class _SendViewState extends ConsumerState { + static const stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + late final String walletId; late final CryptoCurrency coin; late final ClipboardInterface clipboard; @@ -122,6 +132,7 @@ class _SendViewState extends ConsumerState { late final bool isStellar; late final bool isFiro; + late final bool isEth; Amount? _cachedAmountToSend; String? _address; @@ -150,13 +161,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 +183,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 @@ -238,29 +324,27 @@ class _SendViewState extends ConsumerState { ); final Amount? amount; if (baseAmount != null) { - final Decimal _price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + final _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (_price == Decimal.zero) { + if (_price == null || _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 +365,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; @@ -293,13 +377,11 @@ class _SendViewState extends ConsumerState { _cachedAmountToSend = amount; final price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { baseAmountController.text = (amount.decimal * price) - .toAmount( - fractionDigits: 2, - ) + .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, ); @@ -356,10 +438,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 +452,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 +479,8 @@ class _SendViewState extends ConsumerState { } } - ref.read(pValidSendToAddress.notifier).state = - wallet.cryptoCurrency.validateAddress(address ?? ""); + ref.read(pValidSendToAddress.notifier).state = wallet.cryptoCurrency + .validateAddress(address ?? ""); } } @@ -452,9 +521,9 @@ class _SendViewState extends ConsumerState { final wallet = ref.read(pWallets).getWallet(walletId); final feeObject = await wallet.fees; - late final int feeRate; + late final BigInt feeRate; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: feeRate = feeObject.fast; break; @@ -465,13 +534,13 @@ class _SendViewState extends ConsumerState { feeRate = feeObject.slow; break; default: - feeRate = -1; + feeRate = BigInt.from(-1); } Amount fee; if (coin is Monero) { lib_monero.TransactionPriority specialMoneroId; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: specialMoneroId = lib_monero.TransactionPriority.high; break; @@ -485,12 +554,13 @@ class _SendViewState extends ConsumerState { throw ArgumentError("custom fee not available for monero"); } - fee = await wallet.estimateFeeFor(amount, specialMoneroId.value); - cachedFees[amount] = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + fee = await wallet.estimateFeeFor( + amount, + BigInt.from(specialMoneroId.value), + ); + cachedFees[amount] = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees[amount]!; } else if (isFiro) { @@ -499,39 +569,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 +600,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 +649,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 +663,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 +692,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,16 +707,12 @@ class _SendViewState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); Future txDataFuture; if (isPaynymSend) { - final feeRate = ref.read(feeRateTypeStateProvider); + final feeRate = ref.read(feeRateTypeMobileStateProvider); txDataFuture = (wallet as PaynymInterface).preparePaymentCodeSend( txData: TxData( paynymAccountLite: widget.accountLite!, @@ -670,13 +723,14 @@ class _SendViewState extends ConsumerState { isChange: false, ), ], - satsPerVByte: isCustomFee ? customFeeRate : null, + satsPerVByte: isCustomFee.value ? 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) { @@ -693,28 +747,26 @@ class _SendViewState extends ConsumerState { isChange: false, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), - satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (coinControlEnabled && selectedUTXOs.isNotEmpty) - ? selectedUTXOs - : null, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : 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, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + utxos: + (coinControlEnabled && selectedUTXOs.isNotEmpty) + ? selectedUTXOs + : null, ), ); } @@ -724,11 +776,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 +785,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 +810,22 @@ 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, + feeRateType: ref.read(feeRateTypeMobileStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + ethEIP1559Fee: ethFee, + 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 +833,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 +850,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 +886,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 +928,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,18 +953,88 @@ 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(); } bool get isPaynymSend => widget.accountLite != null; - bool isCustomFee = false; - + final isCustomFee = ValueNotifier(false); int customFeeRate = 1; + EthEIP1559Fee? ethFee; + + late final bool hasFees; + + void _onSendToAddressPasteButtonPressed() 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; + }); + } + } + } + + void _onFeeSelectPressed() { + 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.value) { + setState(() { + isCustomFee.value = true; + }); + } + return; + } + + _setCurrentFee(fee, true); + setState(() { + _calculateFeesFuture = Future(() => fee); + if (isCustomFee.value) { + isCustomFee.value = false; + } + }); + }, + ), + ); + } @override void initState() { @@ -932,16 +1043,22 @@ class _SendViewState extends ConsumerState { ref.refresh(feeSheetSessionCacheProvider); ref.refresh(pIsExchangeAddress); }); + isCustomFee.addListener(() { + if (!isCustomFee.value) ethFee = null; + }); + hasFees = coin is! Epiccash && coin is! NanoCurrency && coin is! Tezos; _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; scanner = widget.barcodeScanner; isStellar = coin is Stellar; isFiro = coin is Firo; + isEth = coin is Ethereum; sendToController = TextEditingController(); cryptoAmountController = TextEditingController(); @@ -962,10 +1079,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(); @@ -1040,6 +1156,7 @@ class _SendViewState extends ConsumerState { _cryptoFocus.dispose(); _baseFocus.dispose(); _memoFocus.dispose(); + isCustomFee.dispose(); super.dispose(); } @@ -1051,7 +1168,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 +1193,7 @@ class _SendViewState extends ConsumerState { }); } else { setState(() { - _calculateFeesFuture = calculateFees( - ref.read(pSendAmount)!, - ); + _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); }); } @@ -1112,6 +1228,15 @@ class _SendViewState extends ConsumerState { }); } + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -1135,11 +1260,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 +1275,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 +1288,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 +1311,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 +1336,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,24 +1394,17 @@ 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))}", - style: STextStyles.subtitle( - context, - ).copyWith( - fontSize: 8, + if (price != null) + Text( + "${(amount.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle( + context, + ).copyWith(fontSize: 8), + textAlign: TextAlign.right, ), - textAlign: TextAlign.right, - ), ], ), ), @@ -1297,9 +1415,7 @@ class _SendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1327,9 +1443,7 @@ class _SendViewState extends ConsumerState { // ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (isPaynymSend) TextField( key: const Key("sendViewPaynymAddressFieldKey"), @@ -1359,10 +1473,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 +1486,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 +1517,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 +1528,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 +1591,7 @@ class _SendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), if (isStellar || (ref.watch(pValidSparkSendToAddress) && ref.watch( @@ -1558,9 +1627,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 +1638,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 +1693,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 +1747,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 +1761,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 +1784,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 +1802,10 @@ class _SendViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: (_) => - FiroBalanceSelectionSheet( - walletId: walletId, - ), + builder: + (_) => FiroBalanceSelectionSheet( + walletId: walletId, + ), ); }, child: Row( @@ -1750,9 +1820,7 @@ class _SendViewState extends ConsumerState { context, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Builder( builder: (_) { final Amount amount; @@ -1763,31 +1831,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 +1867,11 @@ class _SendViewState extends ConsumerState { .watch( pAmountFormatter(coin), ) - .format( - amount, - ), + .format(amount), style: STextStyles.itemSubtitle( - context, - ), + context, + ), ); }, ), @@ -1812,9 +1881,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 +1892,7 @@ class _SendViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -1843,27 +1911,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 +1957,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 +1968,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 +1982,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 +2027,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 +2062,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 +2085,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 +2101,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 +2124,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 +2151,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 +2185,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,49 +2202,44 @@ 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, - ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) + const SizedBox(height: 12), + if (hasFees) Text( - "Transaction fee (estimated)", + "Transaction fee ${isEth + ? isCustomFee.value + ? "" + : "(max)" + : "(estimated)"}", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) - const SizedBox( - height: 8, - ), - if (coin is! Epiccash && - coin is! NanoCurrency && - coin is! Tezos) + if (hasFees) const SizedBox(height: 8), + if (hasFees) Stack( children: [ TextField( @@ -2195,198 +2255,146 @@ 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, - ), - ), - 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( + .state != + FiroType.public + ? null + : _onFeeSelectPressed, + 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: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ) + : Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch( + feeRateTypeMobileStateProvider + .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.value + ? "" + : "~${snapshot.data!}", + style: + STextStyles.itemSubtitle( + context, + ), + ); + } else { + return AnimatedText( + stringsToLoopThrough: + stringsToLoopThrough, + style: + STextStyles.itemSubtitle( + context, + ), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textSubtitle2, + ), + ], + ), ), ), ], ), - if (isCustomFee) + if (isCustomFee.value && !isEth) Padding( padding: const EdgeInsets.only( bottom: 12, @@ -2399,29 +2407,34 @@ class _SendViewState extends ConsumerState { }, ), ), + if (isCustomFee.value && isEth) + const SizedBox(height: 12), + if (isCustomFee.value && isEth) + EthFeeForm( + minGasLimit: kEthereumMinGasLimit, + stateChanged: (fee) => ethFee = fee, + ), 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: 16), ], ), ), diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 2586d88ee..41cdac17e 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -8,8 +8,8 @@ * */ -import 'package:flutter/material.dart'; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/paymint/fee_object_model.dart'; @@ -32,8 +32,8 @@ import '../../../widgets/animated_text.dart'; final feeSheetSessionCacheProvider = ChangeNotifierProvider((ref) { - return FeeSheetSessionCache(); -}); + return FeeSheetSessionCache(); + }); class FeeSheetSessionCache extends ChangeNotifier { final Map fast = {}; @@ -79,7 +79,7 @@ class _TransactionFeeSelectionSheetState Future feeFor({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, }) async { switch (feeRateType) { @@ -91,27 +91,31 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.high.value, + BigInt.from(lib_monero.TransactionPriority.high.value), ); ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -128,21 +132,25 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.medium.value, + BigInt.from(lib_monero.TransactionPriority.medium.value), ); ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else { @@ -164,26 +172,30 @@ class _TransactionFeeSelectionSheetState if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.normal.value, + BigInt.from(lib_monero.TransactionPriority.normal.value), ); ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -243,17 +255,10 @@ class _TransactionFeeSelectionSheetState return Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.popupBG, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - top: 10, - bottom: 0, - ), + padding: const EdgeInsets.only(left: 24, right: 24, top: 10, bottom: 0), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -261,9 +266,10 @@ class _TransactionFeeSelectionSheetState Center( child: Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -272,13 +278,12 @@ class _TransactionFeeSelectionSheetState height: 4, ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), FutureBuilder( - future: widget.isToken - ? ref.read(pCurrentTokenWallet)!.fees - : wallet.fees, + future: + widget.isToken + ? ref.read(pCurrentTokenWallet)!.fees + : wallet.fees, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -293,19 +298,21 @@ class _TransactionFeeSelectionSheetState style: STextStyles.pageTitleH2(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.fast) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.fast; } - final String? fee = - getAmount(FeeRateType.fast, wallet.info.coin); + final String? fee = getAmount( + FeeRateType.fast, + wallet.info.coin, + ); if (fee != null) { widget.updateChosen(fee); } @@ -323,16 +330,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.fast, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.fast; Navigator.of(context).pop(); @@ -341,9 +356,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -355,15 +368,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -381,15 +393,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -408,9 +412,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -433,19 +435,21 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.average) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.average; } - final String? fee = - getAmount(FeeRateType.average, coin); + final String? fee = getAmount( + FeeRateType.average, + coin, + ); if (fee != null) { widget.updateChosen(fee); } @@ -462,16 +466,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.average, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.average; Navigator.of(context).pop(); }, @@ -479,9 +491,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -493,15 +503,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -519,15 +528,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -546,9 +547,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -571,15 +570,15 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.slow) { - ref.read(feeRateTypeStateProvider.state).state = + ref.read(feeRateTypeMobileStateProvider.state).state = FeeRateType.slow; } final String? fee = getAmount(FeeRateType.slow, coin); @@ -599,16 +598,24 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.slow, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref - .read(feeRateTypeStateProvider.state) + .read( + feeRateTypeMobileStateProvider + .state, + ) .state = FeeRateType.slow; Navigator.of(context).pop(); }, @@ -616,9 +623,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -630,15 +635,14 @@ class _TransactionFeeSelectionSheetState style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), - const SizedBox( - width: 2, - ), + const SizedBox(width: 2), if (feeObject == null) AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: - STextStyles.itemSubtitle(context), + style: STextStyles.itemSubtitle( + context, + ), ), if (feeObject != null) FutureBuilder( @@ -656,15 +660,7 @@ class _TransactionFeeSelectionSheetState ConnectionState.done && snapshot.hasData) { return Text( - "(~${ref.watch( - pAmountFormatter( - coin, - ), - ).format( - snapshot.data!, - indicatePrecisionLoss: - false, - )})", + "(~${ref.watch(pAmountFormatter(coin)).format(snapshot.data!, indicatePrecisionLoss: false)})", style: STextStyles.itemSubtitle( context, ), @@ -683,9 +679,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (feeObject == null && coin is! Ethereum) AnimatedText( stringsToLoopThrough: @@ -708,17 +702,18 @@ class _TransactionFeeSelectionSheetState ), ), ), - const SizedBox( - height: 24, - ), - if (wallet is ElectrumXInterface) + const SizedBox(height: 24), + if (wallet is ElectrumXInterface || coin is Ethereum) GestureDetector( onTap: () { final state = - ref.read(feeRateTypeStateProvider.state).state; + ref + .read(feeRateTypeMobileStateProvider.state) + .state; if (state != FeeRateType.custom) { - ref.read(feeRateTypeStateProvider.state).state = - FeeRateType.custom; + ref + .read(feeRateTypeMobileStateProvider.state) + .state = FeeRateType.custom; } widget.updateChosen("custom"); @@ -735,17 +730,23 @@ class _TransactionFeeSelectionSheetState width: 20, height: 20, child: Radio( - activeColor: Theme.of(context) - .extension()! - .radioButtonIconEnabled, + activeColor: + Theme.of(context) + .extension()! + .radioButtonIconEnabled, value: FeeRateType.custom, - groupValue: ref - .watch(feeRateTypeStateProvider.state) - .state, + groupValue: + ref + .watch( + feeRateTypeMobileStateProvider + .state, + ) + .state, onChanged: (x) { ref .read( - feeRateTypeStateProvider.state, + feeRateTypeMobileStateProvider + .state, ) .state = FeeRateType.custom; Navigator.of(context).pop(); @@ -754,9 +755,7 @@ class _TransactionFeeSelectionSheetState ), ], ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -765,15 +764,14 @@ class _TransactionFeeSelectionSheetState children: [ Text( FeeRateType.custom.prettyName, - style: - STextStyles.titleBold12(context), + style: STextStyles.titleBold12( + context, + ), textAlign: TextAlign.left, ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), ], ), ), @@ -781,10 +779,8 @@ class _TransactionFeeSelectionSheetState ), ), ), - if (wallet is ElectrumXInterface) - const SizedBox( - height: 24, - ), + if (wallet is ElectrumXInterface || coin is Ethereum) + const SizedBox(height: 24), ], ); }, @@ -800,7 +796,9 @@ class _TransactionFeeSelectionSheetState switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).fast[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -810,7 +808,9 @@ class _TransactionFeeSelectionSheetState case FeeRateType.average: if (ref.read(feeSheetSessionCacheProvider).average[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).average[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -820,7 +820,9 @@ class _TransactionFeeSelectionSheetState case FeeRateType.slow: if (ref.read(feeSheetSessionCacheProvider).slow[amount] != null) { - return ref.read(pAmountFormatter(coin)).format( + return ref + .read(pAmountFormatter(coin)) + .format( ref.read(feeSheetSessionCacheProvider).slow[amount]!, indicatePrecisionLoss: false, withUnitName: false, @@ -831,7 +833,7 @@ class _TransactionFeeSelectionSheetState return null; } } catch (e, s) { - Logging.instance.w("$e $s", error: e, stackTrace: s,); + Logging.instance.w("$e $s", error: e, stackTrace: s); return null; } } diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 01ae07406..7eaaa43f4 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -33,6 +33,7 @@ import '../../utilities/barcode_scanner_interface.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/eth_commons.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; import '../../utilities/text_styles.dart'; @@ -45,6 +46,7 @@ import '../../wallets/models/tx_data.dart'; import '../../widgets/animated_text.dart'; import '../../widgets/background.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/eth_fee_form.dart'; import '../../widgets/icon_widgets/addressbook_icon.dart'; import '../../widgets/icon_widgets/clipboard_icon.dart'; import '../../widgets/icon_widgets/eth_token_icon.dart'; @@ -119,6 +121,10 @@ class _TokenSendViewState extends ConsumerState { late Future _calculateFeesFuture; String cachedFees = ""; + final isCustomFee = ValueNotifier(false); + + EthEIP1559Fee? ethFee; + void _onTokenSendViewPasteAddressFieldButtonPressed() async { final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); if (data?.text != null && data!.text!.isNotEmpty) { @@ -183,10 +189,12 @@ class _TokenSendViewState extends ConsumerState { // autofill amount field if (paymentData.amount != null) { - final Amount amount = Decimal.parse(paymentData.amount!).toAmount( - fractionDigits: tokenContract.decimals, - ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( + final Amount amount = Decimal.parse( + paymentData.amount!, + ).toAmount(fractionDigits: tokenContract.decimals); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format( amount, withUnitName: false, indicatePrecisionLoss: false, @@ -231,22 +239,24 @@ class _TokenSendViewState extends ConsumerState { locale: ref.read(localeServiceChangeNotifierProvider).locale, ); if (baseAmount != null) { - final _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - .item1; + final _price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; - if (_price == Decimal.zero) { + if (_price == null || _price == Decimal.zero) { _amountToSend = Amount.zero; } else { - _amountToSend = baseAmount <= Amount.zero - ? Amount.zero - : Amount.fromDecimal( - (baseAmount.decimal / _price).toDecimal( - scaleOnInfinitePrecision: tokenContract.decimals, - ), - fractionDigits: tokenContract.decimals, - ); + _amountToSend = + baseAmount <= Amount.zero + ? Amount.zero + : Amount.fromDecimal( + (baseAmount.decimal / _price).toDecimal( + scaleOnInfinitePrecision: tokenContract.decimals, + ), + fractionDigits: tokenContract.decimals, + ); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -254,10 +264,9 @@ class _TokenSendViewState extends ConsumerState { _cachedAmountToSend = _amountToSend; _cryptoAmountChangeLock = true; - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - _amountToSend!, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(_amountToSend!, withUnitName: false); _cryptoAmountChangeLock = false; } else { _amountToSend = Amount.zero; @@ -275,10 +284,9 @@ class _TokenSendViewState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( - cryptoAmountController.text, - ethContract: tokenContract, - ); + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse(cryptoAmountController.text, ethContract: tokenContract); if (cryptoAmount != null) { _amountToSend = cryptoAmount; if (_cachedAmountToSend != null && @@ -287,16 +295,15 @@ class _TokenSendViewState extends ConsumerState { } _cachedAmountToSend = _amountToSend; - final price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice(tokenContract.address) - .item1; + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + ?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { baseAmountController.text = (_amountToSend!.decimal * price) - .toAmount( - fractionDigits: 2, - ) + .toAmount(fractionDigits: 2) .fiatString( locale: ref.read(localeServiceChangeNotifierProvider).locale, ); @@ -310,7 +317,7 @@ class _TokenSendViewState extends ConsumerState { _cryptoAmountChangedFeeUpdateTimer?.cancel(); _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_baseFocus.hasFocus) { + if (mounted && coin is! Epiccash && !_baseFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees(); }); @@ -322,7 +329,7 @@ class _TokenSendViewState extends ConsumerState { void _baseAmountChanged() { _baseAmountChangedFeeUpdateTimer?.cancel(); _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { - if (coin is! Epiccash && !_cryptoFocus.hasFocus) { + if (mounted && coin is! Epiccash && !_cryptoFocus.hasFocus) { setState(() { _calculateFeesFuture = calculateFees(); }); @@ -331,7 +338,7 @@ class _TokenSendViewState extends ConsumerState { } String? _updateInvalidAddressText(String address) { - if (_data != null && _data!.contactLabel == address) { + if (_data != null && _data.contactLabel == address) { return null; } if (address.isNotEmpty && @@ -359,9 +366,9 @@ class _TokenSendViewState extends ConsumerState { final wallet = ref.read(pCurrentTokenWallet)!; final feeObject = await wallet.fees; - late final int feeRate; + late final BigInt feeRate; - switch (ref.read(feeRateTypeStateProvider.state).state) { + switch (ref.read(feeRateTypeMobileStateProvider.state).state) { case FeeRateType.fast: feeRate = feeObject.fast; break; @@ -372,15 +379,13 @@ class _TokenSendViewState extends ConsumerState { feeRate = feeObject.slow; break; default: - feeRate = -1; + feeRate = BigInt.from(-1); } final Amount fee = await wallet.estimateFeeFor(Amount.zero, feeRate); - cachedFees = ref.read(pAmountFormatter(coin)).format( - fee, - withUnitName: true, - indicatePrecisionLoss: false, - ); + cachedFees = ref + .read(pAmountFormatter(coin)) + .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFees; } @@ -388,9 +393,7 @@ class _TokenSendViewState 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 tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -471,33 +474,21 @@ class _TokenSendViewState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - feeRateType: ref.read(feeRateTypeStateProvider), + recipients: [(address: _address!, amount: amount, isChange: false)], + feeRateType: ref.read(feeRateTypeMobileStateProvider), note: noteController.text, + ethEIP1559Fee: ethFee, ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -509,13 +500,14 @@ class _TokenSendViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData, - walletId: walletId, - isTokenTx: true, - onSuccess: clearSendForm, - routeOnSuccessName: TokenView.routeName, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData, + walletId: walletId, + isTokenTx: true, + onSuccess: clearSendForm, + routeOnSuccessName: TokenView.routeName, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -545,9 +537,10 @@ class _TokenSendViewState extends ConsumerState { child: Text( "Ok", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), onPressed: () { @@ -578,6 +571,9 @@ class _TokenSendViewState extends ConsumerState { @override void initState() { ref.refresh(feeSheetSessionCacheProvider); + isCustomFee.addListener(() { + if (!isCustomFee.value) ethFee = null; + }); _calculateFeesFuture = calculateFees(); _data = widget.autoFillData; @@ -598,11 +594,11 @@ class _TokenSendViewState extends ConsumerState { baseAmountController.addListener(_baseAmountChanged); if (_data != null) { - if (_data!.amount != null) { - cryptoAmountController.text = _data!.amount!.toString(); + if (_data.amount != null) { + cryptoAmountController.text = _data.amount!.toString(); } - sendToController.text = _data!.contactLabel; - _address = _data!.address.trim(); + sendToController.text = _data.contactLabel; + _address = _data.address.trim(); _addressToggleFlag = true; } @@ -627,6 +623,7 @@ class _TokenSendViewState extends ConsumerState { _addressFocusNode.dispose(); _cryptoFocus.dispose(); _baseFocus.dispose(); + isCustomFee.dispose(); super.dispose(); } @@ -637,6 +634,15 @@ class _TokenSendViewState extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(tokenContract.address)?.value, + ), + ); + } + return Background( child: Scaffold( backgroundColor: Theme.of(context).extension()!.background, @@ -660,11 +666,7 @@ class _TokenSendViewState 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( @@ -679,9 +681,10 @@ class _TokenSendViewState extends ConsumerState { children: [ Container( decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), @@ -693,24 +696,24 @@ class _TokenSendViewState extends ConsumerState { EthTokenIcon( contractAddress: tokenContract.address, ), - 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, ), Text( "Available balance", - style: STextStyles.label(context) - .copyWith(fontSize: 10), + style: STextStyles.label( + context, + ).copyWith(fontSize: 10), ), ], ), @@ -722,13 +725,11 @@ class _TokenSendViewState extends ConsumerState { .format( ref .read( - pTokenBalance( - ( - walletId: widget.walletId, - contractAddress: - tokenContract.address, - ), - ), + pTokenBalance(( + walletId: widget.walletId, + contractAddress: + tokenContract.address, + )), ) .spendable, ethContract: tokenContract, @@ -748,48 +749,30 @@ class _TokenSendViewState extends ConsumerState { .format( ref .watch( - pTokenBalance( - ( - walletId: - widget.walletId, - contractAddress: - tokenContract - .address, - ), - ), - ) - .spendable, - ethContract: tokenContract, - ), - style: - STextStyles.titleBold12(context) - .copyWith( - fontSize: 10, - ), - textAlign: TextAlign.right, - ), - Text( - "${(ref.watch( - pTokenBalance( - ( + pTokenBalance(( walletId: widget.walletId, contractAddress: tokenContract .address, - ), - ), - ).spendable.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", - style: STextStyles.subtitle(context) - .copyWith( - fontSize: 8, - ), + )), + ) + .spendable, + ethContract: tokenContract, + ), + style: STextStyles.titleBold12( + context, + ).copyWith(fontSize: 10), textAlign: TextAlign.right, ), + if (price != null) + Text( + "${(ref.watch(pTokenBalance((walletId: widget.walletId, contractAddress: tokenContract.address))).spendable.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle( + context, + ).copyWith(fontSize: 8), + textAlign: TextAlign.right, + ), ], ), ), @@ -798,17 +781,13 @@ class _TokenSendViewState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Text( "Send to", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -850,9 +829,10 @@ class _TokenSendViewState 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: @@ -860,33 +840,33 @@ class _TokenSendViewState extends ConsumerState { children: [ _addressToggleFlag ? TextFieldIconButton( - key: const Key( - "tokenSendViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend, - ); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "tokenSendViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, + _amountToSend, + ); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "tokenSendViewPasteAddressFieldButtonKey", - ), - onTap: - _onTokenSendViewPasteAddressFieldButtonPressed, - child: sendToController - .text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "tokenSendViewPasteAddressFieldButtonKey", ), + onTap: + _onTokenSendViewPasteAddressFieldButtonPressed, + child: + sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key( @@ -935,11 +915,13 @@ class _TokenSendViewState 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, ), ), ), @@ -947,9 +929,7 @@ class _TokenSendViewState extends ConsumerState { } }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -972,27 +952,28 @@ class _TokenSendViewState 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( @@ -1014,10 +995,9 @@ class _TokenSendViewState 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( @@ -1026,11 +1006,13 @@ class _TokenSendViewState extends ConsumerState { ref .watch(pAmountUnit(coin)) .unitForContract(tokenContract), - style: STextStyles.smallMed14(context) - .copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + style: STextStyles.smallMed14( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .accentColorDark, ), ), ), @@ -1038,28 +1020,29 @@ class _TokenSendViewState 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( @@ -1081,41 +1064,39 @@ class _TokenSendViewState 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, ), ), ), ), ), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Text( "Note (optional)", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1132,41 +1113,38 @@ class _TokenSendViewState 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, - ), - if (coin is! Epiccash) - Text( - "Transaction fee (estimated)", - style: STextStyles.smallMed12(context), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 8, + const SizedBox(height: 12), + Text( + "Transaction fee ${isCustomFee.value ? "" : "(max)"}", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, ), + const SizedBox(height: 8), Stack( children: [ TextField( @@ -1182,9 +1160,10 @@ class _TokenSendViewState 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, @@ -1199,25 +1178,39 @@ class _TokenSendViewState extends ConsumerState { top: Radius.circular(20), ), ), - builder: (_) => - TransactionFeeSelectionSheet( - walletId: walletId, - isToken: true, - amount: (Decimal.tryParse( - cryptoAmountController.text, - ) ?? - Decimal.zero) - .toAmount( - fractionDigits: - tokenContract.decimals, - ), - updateChosen: (String fee) { - setState(() { - _calculateFeesFuture = - Future(() => fee); - }); - }, - ), + builder: + (_) => TransactionFeeSelectionSheet( + walletId: walletId, + isToken: true, + amount: (Decimal.tryParse( + cryptoAmountController + .text, + ) ?? + Decimal.zero) + .toAmount( + fractionDigits: + tokenContract.decimals, + ), + updateChosen: (String fee) { + if (fee == "custom") { + if (!isCustomFee.value) { + setState(() { + isCustomFee.value = true; + }); + } + return; + } + + setState(() { + _calculateFeesFuture = Future( + () => fee, + ); + if (isCustomFee.value) { + isCustomFee.value = false; + } + }); + }, + ), ); }, child: Row( @@ -1229,7 +1222,7 @@ class _TokenSendViewState extends ConsumerState { Text( ref .watch( - feeRateTypeStateProvider + feeRateTypeMobileStateProvider .state, ) .state @@ -1238,9 +1231,7 @@ class _TokenSendViewState extends ConsumerState { context, ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), FutureBuilder( future: _calculateFeesFuture, builder: (context, snapshot) { @@ -1248,11 +1239,13 @@ class _TokenSendViewState extends ConsumerState { ConnectionState.done && snapshot.hasData) { return Text( - "~${snapshot.data!}", + isCustomFee.value + ? "" + : "~${snapshot.data!}", style: STextStyles.itemSubtitle( - context, - ), + context, + ), ); } else { return AnimatedText( @@ -1264,8 +1257,8 @@ class _TokenSendViewState extends ConsumerState { ], style: STextStyles.itemSubtitle( - context, - ), + context, + ), ); } }, @@ -1276,9 +1269,12 @@ class _TokenSendViewState extends ConsumerState { Assets.svg.chevronDown, width: 8, height: 4, - color: Theme.of(context) - .extension()! - .textSubtitle2, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .textSubtitle2, + BlendMode.srcIn, + ), ), ], ), @@ -1286,37 +1282,43 @@ class _TokenSendViewState extends ConsumerState { ), ], ), + if (isCustomFee.value) const SizedBox(height: 12), + if (isCustomFee.value) + EthFeeForm( + minGasLimit: kEthereumTokenMinGasLimit, + stateChanged: (value) => ethFee = value, + ), const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), TextButton( - onPressed: ref - .watch( - previewTokenTxButtonStateProvider.state, - ) - .state - ? _previewTransaction - : null, - style: ref - .watch( - previewTokenTxButtonStateProvider.state, - ) - .state - ? Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context) - : Theme.of(context) - .extension()! - .getPrimaryDisabledButtonStyle(context), + onPressed: + ref + .watch( + previewTokenTxButtonStateProvider + .state, + ) + .state + ? _previewTransaction + : null, + style: + ref + .watch( + previewTokenTxButtonStateProvider + .state, + ) + .state + ? 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: 16), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 4fdeec183..5703db754 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -241,6 +241,8 @@ class _AddEditNodeViewState extends ConsumerState { final plainEnabled = formData.netOption == TorPlainNetworkOption.clear || formData.netOption == TorPlainNetworkOption.both; + + final forceNoTor = formData.forceNoTor ?? false; switch (viewType) { case AddEditNodeViewType.add: @@ -258,6 +260,7 @@ class _AddEditNodeViewState extends ConsumerState { isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, + forceNoTor: forceNoTor, ); await ref @@ -285,6 +288,7 @@ class _AddEditNodeViewState extends ConsumerState { isDown: false, torEnabled: torEnabled, clearnetEnabled: plainEnabled, + forceNoTor: forceNoTor, ); await ref @@ -744,7 +748,7 @@ class _AddEditNodeViewState extends ConsumerState { class NodeFormData { String? name, host, login, password; int? port; - bool? useSSL, isFailover, trusted; + bool? useSSL, isFailover, trusted, forceNoTor; TorPlainNetworkOption? netOption; @override @@ -793,6 +797,7 @@ class _NodeFormState extends ConsumerState { bool _useSSL = false; bool _isFailover = false; bool _trusted = false; + bool _forceNoTor = false; int? port; late bool enableSSLCheckbox; late TorPlainNetworkOption netOption; @@ -851,6 +856,7 @@ class _NodeFormState extends ConsumerState { ref.read(nodeFormDataProvider).isFailover = _isFailover; ref.read(nodeFormDataProvider).trusted = _trusted; ref.read(nodeFormDataProvider).netOption = netOption; + ref.read(nodeFormDataProvider).forceNoTor = _forceNoTor; } @override @@ -885,6 +891,7 @@ class _NodeFormState extends ConsumerState { _useSSL = node.useSSL; _isFailover = node.isFailover; _trusted = node.trusted ?? false; + _forceNoTor = node.forceNoTor ?? false; if (node.torEnabled && !node.clearnetEnabled) { netOption = TorPlainNetworkOption.tor; @@ -1445,9 +1452,48 @@ class _NodeFormState extends ConsumerState { ), ], ), + if (widget.coin is CryptonoteCurrency && _isLocalNode()) + const SizedBox(height: 8), + if (widget.coin is CryptonoteCurrency && _isLocalNode()) + Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: Checkbox( + fillColor: + !widget.readOnly + ? null + : MaterialStateProperty.all( + Theme.of( + context, + ).extension()!.checkboxBGDisabled, + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: _forceNoTor, + onChanged: + !widget.readOnly + ? (newValue) { + setState(() { + _forceNoTor = newValue!; + }); + _updateState(); + } + : null, + ), + ), + const SizedBox(width: 12), + Text("Bypass TOR", style: STextStyles.itemSubtitle12(context)), + ], + ), ], ); } + + bool _isLocalNode() { + final host = _hostController.text.toLowerCase(); + return host.contains("127.0.0.1") || host.contains("localhost"); + } } class RadioTextButton extends StatelessWidget { diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart index 56c87e5c4..572c7d7a2 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart @@ -320,7 +320,8 @@ class _NodeDetailsViewState extends ConsumerState { ..login = node.loginName ..port = node.port ..isFailover = node.isFailover - ..netOption = netOption; + ..netOption = netOption + ..forceNoTor = node.forceNoTor; nodeFormData.password = await node.getPassword( ref.read(secureStoreProvider), ); @@ -396,6 +397,7 @@ class _NodeDetailsViewState extends ConsumerState { TorPlainNetworkOption.clear || ref.read(nodeFormDataProvider).netOption == TorPlainNetworkOption.both, + forceNoTor: ref.read(nodeFormDataProvider).forceNoTor, ); await ref diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index b6e872bd4..679ccc84c 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -141,10 +141,13 @@ abstract class SWB { if (!backupFile.existsSync()) { final String jsonBackup = plaintext; final Uint8List content = Uint8List.fromList(utf8.encode(jsonBackup)); - final Uint8List encryptedContent = - await encryptWithPassphrase(passphrase, content); - backupFile - .writeAsStringSync(Format.uint8listToString(encryptedContent)); + final Uint8List encryptedContent = await encryptWithPassphrase( + passphrase, + content, + ); + backupFile.writeAsStringSync( + Format.uint8listToString(encryptedContent), + ); } Logging.instance.d(backupFile.absolute); return true; @@ -170,8 +173,9 @@ abstract class SWB { content, version: adkVersion, ); - backupFile - .writeAsStringSync(Format.uint8listToString(encryptedContent)); + backupFile.writeAsStringSync( + Format.uint8listToString(encryptedContent), + ); } Logging.instance.d(backupFile.absolute); return true; @@ -207,8 +211,10 @@ abstract class SWB { final encryptedBytes = Format.stringToUint8List(encryptedText); - final decryptedContent = - await decryptWithPassphrase(passphrase, encryptedBytes); + final decryptedContent = await decryptWithPassphrase( + passphrase, + encryptedBytes, + ); final jsonBackup = utf8.decode(decryptedContent); return jsonBackup; @@ -225,50 +231,41 @@ abstract class SWB { Logging.instance.d("Starting createStackWalletJSON..."); final _wallets = Wallets.sharedInstance; final Map backupJson = {}; - final NodeService nodeService = - NodeService(secureStorageInterface: secureStorage); + final NodeService nodeService = NodeService( + secureStorageInterface: secureStorage, + ); final _secureStore = secureStorage; - Logging.instance.d( - "createStackWalletJSON awaiting DB.instance.mutex...", - ); + Logging.instance.d("createStackWalletJSON awaiting DB.instance.mutex..."); // prevent modification of data await DB.instance.mutex.protect(() async { - Logging.instance.i( - "...createStackWalletJSON DB.instance.mutex acquired", - ); - Logging.instance.i( - "SWB backing up nodes", - ); + Logging.instance.i("...createStackWalletJSON DB.instance.mutex acquired"); + Logging.instance.i("SWB backing up nodes"); try { - final primaryNodes = nodeService.primaryNodes.map((e) async { - final map = e.toMap(); - map["password"] = await e.getPassword(_secureStore); - return map; - }).toList(); + final primaryNodes = + nodeService.primaryNodes.map((e) async { + final map = e.toMap(); + map["password"] = await e.getPassword(_secureStore); + return map; + }).toList(); backupJson['primaryNodes'] = await Future.wait(primaryNodes); } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); } try { - final nodesFuture = nodeService.nodes.map((e) async { - final map = e.toMap(); - map["password"] = await e.getPassword(_secureStore); - return map; - }).toList(); + final nodesFuture = + nodeService.nodes.map((e) async { + final map = e.toMap(); + map["password"] = await e.getPassword(_secureStore); + return map; + }).toList(); final nodes = await Future.wait(nodesFuture); backupJson['nodes'] = nodes; } catch (e, s) { Logging.instance.e("", error: e, stackTrace: s); } - Logging.instance.d( - "SWB backing up prefs", - ); + Logging.instance.d("SWB backing up prefs"); final Map prefs = {}; final _prefs = Prefs.instance; @@ -289,18 +286,14 @@ abstract class SWB { backupJson['prefs'] = prefs; - Logging.instance.d( - "SWB backing up addressbook", - ); + Logging.instance.d("SWB backing up addressbook"); final AddressBookService addressBookService = AddressBookService(); final addresses = addressBookService.contacts; backupJson['addressBookEntries'] = addresses.map((e) => e.toMap()).toList(); - Logging.instance.d( - "SWB backing up wallets", - ); + Logging.instance.d("SWB backing up wallets"); final List backupWallets = []; for (final wallet in _wallets.wallets) { @@ -349,14 +342,15 @@ abstract class SWB { backupWallet['restoreHeight'] = wallet.info.restoreHeight; - final isarNotes = await MainDB.instance.isar.transactionNotes - .where() - .walletIdEqualTo(wallet.walletId) - .findAll(); + final isarNotes = + await MainDB.instance.isar.transactionNotes + .where() + .walletIdEqualTo(wallet.walletId) + .findAll(); - final notes = isarNotes - .asMap() - .map((key, value) => MapEntry(value.txid, value.value)); + final notes = isarNotes.asMap().map( + (key, value) => MapEntry(value.txid, value.value), + ); backupWallet['notes'] = notes; @@ -364,14 +358,13 @@ abstract class SWB { } backupJson['wallets'] = backupWallets; - Logging.instance.d( - "SWB backing up trades", - ); + Logging.instance.d("SWB backing up trades"); // back up trade history final tradesService = TradesService(); - final trades = - tradesService.trades.map((e) => e.toMap()).toList(growable: false); + final trades = tradesService.trades + .map((e) => e.toMap()) + .toList(growable: false); backupJson["tradeHistory"] = trades; // back up trade history lookup data for trades send from stack wallet @@ -380,18 +373,14 @@ abstract class SWB { tradeTxidLookupDataService.all.map((e) => e.toMap()).toList(); backupJson["tradeTxidLookupData"] = lookupData; - Logging.instance.d( - "SWB backing up trade notes", - ); + Logging.instance.d("SWB backing up trade notes"); // back up trade notes final tradeNotesService = TradeNotesService(); final tradeNotes = tradeNotesService.all; backupJson["tradeNotes"] = tradeNotes; }); - Logging.instance.d( - "createStackWalletJSON DB.instance.mutex released", - ); + Logging.instance.d("createStackWalletJSON DB.instance.mutex released"); // // back up notifications data // final notificationsService = NotificationsService(); @@ -473,9 +462,7 @@ abstract class SWB { knownSalts: [], participants: participants, myName: myName, - threshold: frost.multisigThreshold( - multisigConfig: multisigConfig, - ), + threshold: frost.multisigThreshold(multisigConfig: multisigConfig), ); await MainDB.instance.isar.writeTxn(() async { @@ -507,7 +494,7 @@ abstract class SWB { case const (WowneroWallet): await (wallet as WowneroWallet).init(isRestore: true); break; - + case const (XelisWallet): await (wallet as XelisWallet).init(isRestore: true); break; @@ -547,8 +534,9 @@ abstract class SWB { } // restore notes - final notesMap = - Map.from(walletbackup["notes"] as Map? ?? {}); + final notesMap = Map.from( + walletbackup["notes"] as Map? ?? {}, + ); final List notes = []; for (final key in notesMap.keys) { @@ -601,11 +589,7 @@ abstract class SWB { mnemonicPassphrase: mnemonicPassphrase, ); } catch (e, s) { - Logging.instance.i( - "", - error: e, - stackTrace: s, - ); + Logging.instance.i("", error: e, stackTrace: s); uiState?.update( walletId: info.walletId, restoringStatus: StackRestoringStatus.failed, @@ -639,17 +623,13 @@ abstract class SWB { uiState?.preferences = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring prefs", - ); + Logging.instance.d("SWB restoring prefs"); await _restorePrefs(prefs); uiState?.preferences = StackRestoringStatus.success; uiState?.addressBook = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring addressbook", - ); + Logging.instance.d("SWB restoring addressbook"); if (addressBookEntries != null) { await _restoreAddressBook(addressBookEntries); } @@ -657,40 +637,28 @@ abstract class SWB { uiState?.addressBook = StackRestoringStatus.success; uiState?.nodes = StackRestoringStatus.restoring; - Logging.instance.d( - "SWB restoring nodes", - ); - await _restoreNodes( - nodes, - primaryNodes, - secureStorageInterface, - ); + Logging.instance.d("SWB restoring nodes"); + await _restoreNodes(nodes, primaryNodes, secureStorageInterface); uiState?.nodes = StackRestoringStatus.success; uiState?.trades = StackRestoringStatus.restoring; // restore trade history if (trades != null) { - Logging.instance.d( - "SWB restoring trades", - ); + Logging.instance.d("SWB restoring trades"); await _restoreTrades(trades); } // restore trade history lookup data for trades send from stack wallet if (tradeTxidLookupData != null) { - Logging.instance.d( - "SWB restoring trade look up data", - ); + Logging.instance.d("SWB restoring trade look up data"); await _restoreTradesLookUpData(tradeTxidLookupData, oldToNewWalletIdMap); } // restore trade notes if (tradeNotes != null) { - Logging.instance.d( - "SWB restoring trade notes", - ); + Logging.instance.d("SWB restoring trade notes"); await _restoreTradesNotes(tradeNotes); } @@ -722,22 +690,22 @@ abstract class SWB { ) async { if (!Platform.isLinux) await WakelockPlus.enable(); - Logging.instance.d( - "SWB creating temp backup", - ); - final preRestoreJSON = - await createStackWalletJSON(secureStorage: secureStorageInterface); - Logging.instance.d( - "SWB temp backup created", + Logging.instance.d("SWB creating temp backup"); + final preRestoreJSON = await createStackWalletJSON( + secureStorage: secureStorageInterface, ); + Logging.instance.d("SWB temp backup created"); - final List _currentWalletIds = await MainDB.instance.isar.walletInfo - .where() - .walletIdProperty() - .findAll(); + final List _currentWalletIds = + await MainDB.instance.isar.walletInfo + .where() + .walletIdProperty() + .findAll(); - final preRestoreState = - PreRestoreState(_currentWalletIds.toSet(), preRestoreJSON); + final preRestoreState = PreRestoreState( + _currentWalletIds.toSet(), + preRestoreJSON, + ); final Map oldToNewWalletIdMap = {}; @@ -759,10 +727,7 @@ abstract class SWB { // basic cancel check here // no reverting required yet as nothing has been written to store - if (_checkShouldCancel( - null, - secureStorageInterface, - )) { + if (_checkShouldCancel(null, secureStorageInterface)) { return false; } @@ -774,10 +739,7 @@ abstract class SWB { ); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -794,10 +756,7 @@ abstract class SWB { for (final walletbackup in wallets) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -818,8 +777,9 @@ abstract class SWB { Map? otherData; try { if (walletbackup["otherDataJsonString"] is String) { - final data = - jsonDecode(walletbackup["otherDataJsonString"] as String); + final data = jsonDecode( + walletbackup["otherDataJsonString"] as String, + ); otherData = Map.from(data as Map); } } catch (e, s) { @@ -859,19 +819,13 @@ abstract class SWB { // final failovers = nodeService.failoverNodesFor(coin: coin); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } managers.add(Tuple2(walletbackup, info)); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -884,10 +838,7 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } @@ -898,10 +849,7 @@ abstract class SWB { // start restoring wallets for (final tuple in managers) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } final bools = await _asyncRestore( @@ -915,19 +863,13 @@ abstract class SWB { } // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } for (final Future status in restoreStatuses) { // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } await status; @@ -935,19 +877,17 @@ abstract class SWB { if (!Platform.isLinux) await WakelockPlus.disable(); // check if cancel was requested and restore previous state - if (_checkShouldCancel( - preRestoreState, - secureStorageInterface, - )) { + if (_checkShouldCancel(preRestoreState, secureStorageInterface)) { return false; } - Logging.instance.d( - "done with SWB restore", - ); + Logging.instance.d("done with SWB restore"); - await Wallets.sharedInstance - .loadAfterStackRestore(_prefs, uiState?.wallets ?? [], Util.isDesktop); + await Wallets.sharedInstance.loadAfterStackRestore( + _prefs, + uiState?.wallets ?? [], + Util.isDesktop, + ); return true; } @@ -998,11 +938,12 @@ abstract class SWB { // ensure this contact's data matches the pre restore state final List addresses = []; for (final address in (contact['addresses'] as List)) { - final entry = ContactAddressEntry() - ..coinName = address['coin'] as String - ..address = address['address'] as String - ..label = address['label'] as String - ..other = address['other'] as String?; + final entry = + ContactAddressEntry() + ..coinName = address['coin'] as String + ..address = address['address'] as String + ..label = address['label'] as String + ..other = address['other'] as String?; try { entry.coin; @@ -1011,9 +952,7 @@ abstract class SWB { continue; } - addresses.add( - entry, - ); + addresses.add(entry); } await addressBookService.editContact( ContactEntry( @@ -1174,22 +1113,22 @@ abstract class SWB { } // finally remove any added wallets - final allWalletIds = (await MainDB.instance.isar.walletInfo - .where() - .walletIdProperty() - .findAll()) - .toSet(); + final allWalletIds = + (await MainDB.instance.isar.walletInfo + .where() + .walletIdProperty() + .findAll()) + .toSet(); final walletIdsToDelete = allWalletIds.difference(revertToState.walletIds); await MainDB.instance.isar.writeTxn(() async { - await MainDB.instance.isar.walletInfo - .deleteAllByWalletId(walletIdsToDelete.toList()); + await MainDB.instance.isar.walletInfo.deleteAllByWalletId( + walletIdsToDelete.toList(), + ); }); _cancelCompleter!.complete(); _shouldCancelRestore = false; - Logging.instance.d( - "Revert SWB complete", - ); + Logging.instance.d("Revert SWB complete"); } static Future _restorePrefs(Map prefs) async { @@ -1201,9 +1140,10 @@ abstract class SWB { _prefs.language = prefs['language'] as String; _prefs.showFavoriteWallets = prefs['showFavoriteWallets'] as bool; _prefs.wifiOnly = prefs['wifiOnly'] as bool; - _prefs.syncType = prefs['syncType'] == "currentWalletOnly" - ? SyncingType.currentWalletOnly - : prefs['syncType'] == "selectedWalletsAtStartup" + _prefs.syncType = + prefs['syncType'] == "currentWalletOnly" + ? SyncingType.currentWalletOnly + : prefs['syncType'] == "selectedWalletsAtStartup" ? SyncingType.currentWalletOnly : SyncingType.allWalletsOnStartup; // _prefs.walletIdsSyncOnStartup = @@ -1217,8 +1157,9 @@ abstract class SWB { (e) => e.name == (prefs['backupFrequencyType'] as String?), orElse: () => BackupFrequencyType.everyAppStart, ); - _prefs.lastAutoBackup = - DateTime.tryParse(prefs['lastAutoBackup'] as String? ?? ""); + _prefs.lastAutoBackup = DateTime.tryParse( + prefs['lastAutoBackup'] as String? ?? "", + ); } static Future _restoreAddressBook( @@ -1228,11 +1169,12 @@ abstract class SWB { for (final contact in addressBookEntries) { final List addresses = []; for (final address in (contact['addresses'] as List)) { - final entry = ContactAddressEntry() - ..coinName = address['coin'] as String - ..address = address['address'] as String - ..label = address['label'] as String - ..other = address['other'] as String?; + final entry = + ContactAddressEntry() + ..coinName = address['coin'] as String + ..address = address['address'] as String + ..label = address['label'] as String + ..other = address['other'] as String?; try { entry.coin; @@ -1241,9 +1183,7 @@ abstract class SWB { continue; } - addresses.add( - entry, - ); + addresses.add(entry); } if (addresses.isNotEmpty) { await addressBookService.addContact( @@ -1313,53 +1253,35 @@ abstract class SWB { await nodeService.updateDefaults(); } - static Future _restoreTrades( - List trades, - ) async { - final tradesService = TradesService(); - for (int i = 0; i < trades.length - 1; i++) { - ExchangeTransaction? exTx; - try { - exTx = ExchangeTransaction.fromJson(trades[i] as Map); - } catch (e) { - // unneeded log - // Logging.instance.log("$e\n$s", error: e, stackTrace: s,); - } - - Trade trade; - if (exTx != null) { - trade = Trade.fromExchangeTransaction(exTx, false); - } else { - trade = Trade.fromMap(trades[i] as Map); - } - - await tradesService.add( - trade: trade, - shouldNotifyListeners: false, - ); - } - // only call notifyListeners on last one added + static Future _restoreTrades(List trades) async { if (trades.isNotEmpty) { - ExchangeTransaction? exTx; - try { - exTx = - ExchangeTransaction.fromJson(trades.last as Map); - } catch (e) { - // unneeded log - // Logging.instance.log("$e\n$s", level: LogLevel.Warning); - } + final tradesService = TradesService(); + for (int i = 0; i < trades.length; i++) { + // First check for old old database entries + ExchangeTransaction? exTx; + try { + exTx = ExchangeTransaction.fromJson( + trades[i] as Map, + ); + } catch (e) { + // unneeded log + // Logging.instance.log("$e\n$s", error: e, stackTrace: s,); + } - Trade trade; - if (exTx != null) { - trade = Trade.fromExchangeTransaction(exTx, false); - } else { - trade = Trade.fromMap(trades.last as Map); - } + Trade trade; + if (exTx != null) { + trade = Trade.fromExchangeTransaction(exTx, false); + } else { + trade = Trade.fromMap(trades[i] as Map); + } - await tradesService.add( - trade: trade, - shouldNotifyListeners: true, - ); + await tradesService.add( + trade: trade, + shouldNotifyListeners: + i == + trades.length - 1, // only call notifyListeners on last one added + ); + } } } 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..011532708 --- /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) + ?.value; + if (price != null && 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/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart index 4f1eec6d1..745f2a0fd 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -56,10 +56,7 @@ class _MyTokenSelectItemState extends ConsumerState { late final CachedEthTokenBalance cachedBalance; - Future _loadTokenWallet( - BuildContext context, - WidgetRef ref, - ) async { + Future _loadTokenWallet(BuildContext context, WidgetRef ref) async { try { await ref.read(pCurrentTokenWallet)!.init(); return true; @@ -67,20 +64,21 @@ class _MyTokenSelectItemState extends ConsumerState { await showDialog( barrierDismissible: false, context: context, - builder: (context) => BasicDialog( - title: "Failed to load token data", - desktopHeight: double.infinity, - desktopWidth: 450, - rightButton: PrimaryButton( - label: "OK", - onPressed: () { - Navigator.of(context).pop(); - if (!isDesktop) { - Navigator.of(context).pop(); - } - }, - ), - ), + builder: + (context) => BasicDialog( + title: "Failed to load token data", + desktopHeight: double.infinity, + desktopWidth: 450, + rightButton: PrimaryButton( + label: "OK", + onPressed: () { + Navigator.of(context).pop(); + if (!isDesktop) { + Navigator.of(context).pop(); + } + }, + ), + ), ); return false; } @@ -90,11 +88,14 @@ class _MyTokenSelectItemState extends ConsumerState { final old = ref.read(tokenServiceStateProvider); // exit previous if there is one unawaited(old?.exit()); - ref.read(tokenServiceStateProvider.state).state = Wallet.loadTokenWallet( - ethWallet: - ref.read(pWallets).getWallet(widget.walletId) as EthereumWallet, - contract: widget.token, - ) as EthTokenWallet; + ref.read(tokenServiceStateProvider.state).state = + Wallet.loadTokenWallet( + ethWallet: + ref.read(pWallets).getWallet(widget.walletId) + as EthereumWallet, + contract: widget.token, + ) + as EthTokenWallet; final success = await showLoading( whileFuture: _loadTokenWallet(context, ref), @@ -138,17 +139,29 @@ class _MyTokenSelectItemState extends ConsumerState { @override Widget build(BuildContext context) { + String? priceString; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + priceString = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (s) => + s.getTokenPrice(widget.token.address)?.value.toStringAsFixed(2), + ), + ); + } + return RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: MaterialButton( key: Key("walletListItemButtonKey_${widget.token.symbol}"), - padding: isDesktop - ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + padding: + isDesktop + ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), onPressed: _onPressed, child: Row( @@ -157,9 +170,7 @@ class _MyTokenSelectItemState extends ConsumerState { contractAddress: widget.token.address, size: isDesktop ? 32 : 28, ), - SizedBox( - width: isDesktop ? 12 : 10, - ), + SizedBox(width: isDesktop ? 12 : 10), Expanded( child: Consumer( builder: (_, ref, __) { @@ -170,14 +181,17 @@ class _MyTokenSelectItemState extends ConsumerState { children: [ Text( widget.token.name, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.titleBold12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.titleBold12(context), ), const Spacer(), Text( @@ -190,62 +204,52 @@ class _MyTokenSelectItemState extends ConsumerState { .format( ref .watch( - pTokenBalance( - ( - walletId: widget.walletId, - contractAddress: - widget.token.address - ), - ), + pTokenBalance(( + walletId: widget.walletId, + contractAddress: widget.token.address, + )), ) .total, ethContract: widget.token, ), - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( children: [ Text( widget.token.symbol, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), ), const Spacer(), - Text( - "${ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value - .getTokenPrice(widget.token.address) - .item1 - .toStringAsFixed(2), - ), - )} " - "${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), - ), + if (priceString != null) + Text( + "$priceString " + "${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), + ), ], ), ], diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index f5b051ef7..9bebfda49 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -11,6 +11,7 @@ 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'; @@ -53,12 +54,22 @@ class TokenSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final token = - ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract)); + final token = ref.watch( + pCurrentTokenWallet.select((value) => value!.tokenContract), + ); final balance = ref.watch( pTokenBalance((walletId: walletId, contractAddress: token.address)), ); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(token.address)?.value, + ), + ); + } + return Stack( children: [ RoundedContainer( @@ -71,30 +82,26 @@ class TokenSummary extends ConsumerWidget { children: [ SvgPicture.asset( Assets.svg.walletDesktop, - color: Theme.of(context) - .extension()! - .tokenSummaryTextSecondary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextSecondary, width: 12, height: 12, ), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), Text( - ref.watch( - pWalletName(walletId), - ), + ref.watch(pWalletName(walletId)), style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextSecondary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextSecondary, ), ), ], ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -105,58 +112,31 @@ class TokenSummary extends ConsumerWidget { Ethereum(CryptoCurrencyNetwork.main), ), ) - .format( - balance.total, - ethContract: token, - ), + .format(balance.total, ethContract: token), style: STextStyles.pageTitleH1(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), ), - const SizedBox( - width: 10, - ), - CoinTickerTag( - walletId: walletId, - ), + const SizedBox(width: 10), + CoinTickerTag(walletId: walletId), ], ), - const SizedBox( - height: 6, - ), - Text( - "${(balance.total.decimal * ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getTokenPrice(token.address).item1, - ), - )).toAmount( - fractionDigits: 2, - ).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, + if (price != null) const SizedBox(height: 6), + if (price != null) + Text( + "${(balance.total.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle500(context).copyWith( + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), - )}", - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, ), - ), - const SizedBox( - height: 20, - ), - TokenWalletOptions( - walletId: walletId, - tokenContract: token, - ), + const SizedBox(height: 20), + TokenWalletOptions(walletId: walletId, tokenContract: token), ], ), ), @@ -195,11 +175,7 @@ class TokenWalletOptions extends ConsumerWidget { unawaited( Navigator.of(context).pushNamed( WalletInitiatedExchangeView.routeName, - arguments: Tuple3( - walletId, - ethereum, - tokenContract, - ), + arguments: Tuple3(walletId, ethereum, tokenContract), ), ); } @@ -208,10 +184,7 @@ class TokenWalletOptions extends ConsumerWidget { unawaited( Navigator.of(context).pushNamed( BuyInWalletView.routeName, - arguments: Tuple2( - ethereum, - tokenContract, - ), + arguments: Tuple2(ethereum, tokenContract), ), ); } @@ -228,50 +201,35 @@ class TokenWalletOptions extends ConsumerWidget { onPressed: () { Navigator.of(context).pushNamed( ReceiveView.routeName, - arguments: Tuple2( - walletId, - tokenContract, - ), + arguments: Tuple2(walletId, tokenContract), ); }, subLabel: "Receive", iconAssetPathSVG: Assets.svg.arrowDownLeft, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), TokenOptionsButton( onPressed: () { Navigator.of(context).pushNamed( TokenSendView.routeName, - arguments: Tuple3( - walletId, - ethereum, - tokenContract, - ), + arguments: Tuple3(walletId, ethereum, tokenContract), ); }, subLabel: "Send", iconAssetPathSVG: Assets.svg.arrowUpRight, ), if (AppConfig.hasFeature(AppFeature.swap) && showExchange) - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (AppConfig.hasFeature(AppFeature.swap) && showExchange) TokenOptionsButton( onPressed: () => _onExchangePressed(context), subLabel: "Swap", iconAssetPathSVG: ref.watch( - themeProvider.select( - (value) => value.assets.exchange, - ), + themeProvider.select((value) => value.assets.exchange), ), ), if (AppConfig.hasFeature(AppFeature.buy) && showExchange) - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), if (AppConfig.hasFeature(AppFeature.buy) && showExchange) TokenOptionsButton( onPressed: () => _onBuyPressed(context), @@ -320,46 +278,47 @@ class TokenOptionsButton extends StatelessWidget { padding: const EdgeInsets.all(10), child: ConditionalParent( condition: iconSize < 24, - builder: (child) => RoundedContainer( - padding: const EdgeInsets.all(6), - color: Theme.of(context) - .extension()! - .tokenSummaryIcon - .withOpacity(0.4), - radiusMultiplier: 10, - child: Center( - child: child, - ), - ), - child: iconAssetPathSVG.startsWith("assets/") - ? SvgPicture.asset( - iconAssetPathSVG, - color: Theme.of(context) - .extension()! - .tokenSummaryIcon, - width: iconSize, - height: iconSize, - ) - : SvgPicture.file( - File(iconAssetPathSVG), - color: Theme.of(context) - .extension()! - .tokenSummaryIcon, - width: iconSize, - height: iconSize, - ), + builder: + (child) => RoundedContainer( + padding: const EdgeInsets.all(6), + color: Theme.of(context) + .extension()! + .tokenSummaryIcon + .withOpacity(0.4), + radiusMultiplier: 10, + child: Center(child: child), + ), + child: + iconAssetPathSVG.startsWith("assets/") + ? SvgPicture.asset( + iconAssetPathSVG, + color: + Theme.of( + context, + ).extension()!.tokenSummaryIcon, + width: iconSize, + height: iconSize, + ) + : SvgPicture.file( + File(iconAssetPathSVG), + color: + Theme.of( + context, + ).extension()!.tokenSummaryIcon, + width: iconSize, + height: iconSize, + ), ), ), ), - const SizedBox( - height: 6, - ), + const SizedBox(height: 6), Text( subLabel, style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .tokenSummaryTextPrimary, + color: + Theme.of( + context, + ).extension()!.tokenSummaryTextPrimary, ), ), ], @@ -368,10 +327,7 @@ class TokenOptionsButton extends StatelessWidget { } class CoinTickerTag extends ConsumerWidget { - const CoinTickerTag({ - super.key, - required this.walletId, - }); + const CoinTickerTag({super.key, required this.walletId}); final String walletId; @@ -382,11 +338,7 @@ class CoinTickerTag extends ConsumerWidget { radiusMultiplier: 0.25, color: Theme.of(context).extension()!.ethTagBG, child: Text( - ref - .watch( - pWalletCoin(walletId), - ) - .ticker, + ref.watch(pWalletCoin(walletId)).ticker, style: STextStyles.w600_12(context).copyWith( color: Theme.of(context).extension()!.ethTagText, ), diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 08d9152b8..1929a1ca4 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -11,6 +11,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -52,9 +53,7 @@ class WalletSummaryInfo extends ConsumerWidget { useSafeArea: true, isScrollControlled: true, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => WalletBalanceToggleSheet(walletId: walletId), ); @@ -64,9 +63,6 @@ class WalletSummaryInfo extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: $runtimeType"); - final externalCalls = ref.watch( - prefsChangeNotifierProvider.select((value) => value.externalCalls), - ); final coin = ref.watch(pWalletCoin(walletId)); final balance = ref.watch(pWalletBalance(walletId)); @@ -74,14 +70,27 @@ class WalletSummaryInfo extends ConsumerWidget { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); + + ({double change24h, Decimal value})? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); + } final priceTuple = ref.watch( priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin)), ); - final _showAvailable = ref.watch(walletBalanceToggleStateProvider) == + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider) == WalletBalanceToggleState.available; final Amount balanceToShow; @@ -119,23 +128,23 @@ class WalletSummaryInfo extends ConsumerWidget { List? imageBytes; if (coin is Banano) { - imageBytes = (ref.watch(pWallets).getWallet(walletId) as BananoWallet) - .getMonkeyImageBytes(); + imageBytes = + (ref.watch(pWallets).getWallet(walletId) as BananoWallet) + .getMonkeyImageBytes(); } return ConditionalParent( condition: imageBytes != null, - builder: (child) => Stack( - children: [ - Positioned.fill( - left: 150.0, - child: SvgPicture.memory( - Uint8List.fromList(imageBytes!), - ), + builder: + (child) => Stack( + children: [ + Positioned.fill( + left: 150.0, + child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), + ), + child, + ], ), - child, - ], - ), child: Row( children: [ Expanded( @@ -164,20 +173,20 @@ class WalletSummaryInfo extends ConsumerWidget { Text( title, style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), if (!toggleBalance) ...[ - const SizedBox( - width: 4, - ), + const SizedBox(width: 4), SvgPicture.asset( Assets.svg.chevronDown, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, width: 8, height: 4, ), @@ -207,23 +216,21 @@ class WalletSummaryInfo extends ConsumerWidget { ref.watch(pAmountFormatter(coin)).format(balanceToShow), style: STextStyles.pageTitleH1(context).copyWith( fontSize: 24, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), ), - if (externalCalls) + if (price != null) Text( - "${(priceTuple.item1 * balanceToShow.decimal).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "${(price.value * balanceToShow.decimal).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), ), ], @@ -232,9 +239,7 @@ class WalletSummaryInfo extends ConsumerWidget { Column( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 5afd82597..9fcbfc3f4 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -51,17 +52,11 @@ import '../sub_widgets/tx_icon.dart'; import 'transaction_details_view.dart'; import 'transaction_search_filter_view.dart'; -typedef _GroupedTransactions = ({ - String label, - DateTime startDate, - List transactions -}); +typedef _GroupedTransactions = + ({String label, DateTime startDate, List transactions}); class AllTransactionsView extends ConsumerStatefulWidget { - const AllTransactionsView({ - super.key, - required this.walletId, - }); + const AllTransactionsView({super.key, required this.walletId}); static const String routeName = "/allTransactions"; @@ -106,13 +101,14 @@ class _TransactionDetailsViewState extends ConsumerState { // debugPrint("FILTER: $filter"); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions.where((tx) { if (!filter.sent && !filter.received) { @@ -162,15 +158,16 @@ class _TransactionDetailsViewState extends ConsumerState { bool contains = false; // check if address book name contains - contains |= contacts - .where( - (e) => - e.addresses - .where((a) => a.address == tx.address.value?.value) - .isNotEmpty && - e.name.toLowerCase().contains(keyword), - ) - .isNotEmpty; + contains |= + contacts + .where( + (e) => + e.addresses + .where((a) => a.address == tx.address.value?.value) + .isNotEmpty && + e.name.toLowerCase().contains(keyword), + ) + .isNotEmpty; // check if address contains contains |= @@ -195,8 +192,9 @@ class _TransactionDetailsViewState extends ConsumerState { contains |= tx.type.name.toLowerCase().contains(keyword); // check if date contains - contains |= - Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword); + contains |= Format.extractDateFrom( + tx.timestamp, + ).toLowerCase().contains(keyword); return contains; } @@ -210,13 +208,14 @@ class _TransactionDetailsViewState extends ConsumerState { } text = text.toLowerCase(); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions .where((tx) => _isKeywordMatch(tx, text, contacts, notes)) @@ -232,8 +231,11 @@ class _TransactionDetailsViewState extends ConsumerState { final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); final monthYear = "${Constants.monthMap[date.month]} ${date.year}"; if (map[monthYear] == null) { - map[monthYear] = - (label: monthYear, startDate: date, transactions: [tx]); + map[monthYear] = ( + label: monthYear, + startDate: date, + transactions: [tx], + ); } else { map[monthYear]!.transactions.add(tx); } @@ -250,97 +252,99 @@ class _TransactionDetailsViewState extends ConsumerState { return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: true, - background: Theme.of(context).extension()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 12, - ), - Text( - "Transactions", - style: STextStyles.desktopH3(context), - ), - ], - ), - ) - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Transactions", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), - size: 36, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], - color: Theme.of(context) - .extension()! - .background, icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: - ref.read(pWallets).getWallet(walletId).info.coin, - ); - }, + onPressed: Navigator.of(context).pop, ), - ), + const SizedBox(width: 12), + Text("Transactions", style: STextStyles.desktopH3(context)), + ], ), - ], - ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: + ref + .read(pWallets) + .getWallet(walletId) + .info + .coin, + ); + }, + ), + ), + ), + ], + ), body: Padding( padding: EdgeInsets.only( left: isDesktop ? 20 : 12, @@ -355,15 +359,10 @@ class _TransactionDetailsViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 570, - child: child, - ), + builder: (child) => SizedBox(width: 570, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -378,15 +377,18 @@ class _TransactionDetailsViewState extends ConsumerState { _searchString = value; }); }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, @@ -404,35 +406,33 @@ class _TransactionDetailsViewState extends ConsumerState { height: isDesktop ? 20 : 16, ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), ), - if (isDesktop) - const SizedBox( - width: 20, - ), + if (isDesktop) const SizedBox(width: 20), if (isDesktop) SecondaryButton( buttonHeight: ButtonHeight.l, @@ -440,9 +440,10 @@ class _TransactionDetailsViewState extends ConsumerState { label: "Filter", icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -453,9 +454,7 @@ class _TransactionDetailsViewState extends ConsumerState { showDialog( context: context, builder: (context) { - return TransactionSearchFilterView( - coin: coin, - ); + return TransactionSearchFilterView(coin: coin); }, ); } else { @@ -469,25 +468,14 @@ class _TransactionDetailsViewState extends ConsumerState { ], ), ), - if (isDesktop) - const SizedBox( - height: 8, - ), + if (isDesktop) const SizedBox(height: 8), if (isDesktop && ref.watch(transactionFilterProvider.state).state != null) const Padding( - padding: EdgeInsets.symmetric( - vertical: 8, - ), - child: Row( - children: [ - TransactionFilterOptionBar(), - ], - ), + padding: EdgeInsets.symmetric(vertical: 8), + child: Row(children: [TransactionFilterOptionBar()]), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -495,38 +483,40 @@ class _TransactionDetailsViewState extends ConsumerState { ref.watch(transactionFilterProvider.state).state; return FutureBuilder( - future: ref - .watch(mainDBProvider) - .isar - .transactions - .buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [widget.walletId], - ), - ], - // TODO: [prio=med] add filters to wallet or cryptocurrency class - // eth tokens should all be on v2 txn now so this should not be needed here - // filter: widget.contractAddress != null - // ? FilterGroup.and([ - // FilterCondition.equalTo( - // property: r"contractAddress", - // value: widget.contractAddress!, - // ), - // const FilterCondition.equalTo( - // property: r"subType", - // value: TransactionSubType.ethToken, - // ), - // ]) - // : null, - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, - ), - ], - ).findAll(), + future: + ref + .watch(mainDBProvider) + .isar + .transactions + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo( + indexName: 'walletId', + value: [widget.walletId], + ), + ], + // TODO: [prio=med] add filters to wallet or cryptocurrency class + // eth tokens should all be on v2 txn now so this should not be needed here + // filter: widget.contractAddress != null + // ? FilterGroup.and([ + // FilterCondition.equalTo( + // property: r"contractAddress", + // value: widget.contractAddress!, + // ), + // const FilterCondition.equalTo( + // property: r"subType", + // value: TransactionSubType.ethToken, + // ), + // ]) + // : null, + sortBy: [ + const SortProperty( + property: "timestamp", + sort: Sort.desc, + ), + ], + ) + .findAll(), builder: (_, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -555,43 +545,39 @@ class _TransactionDetailsViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.label, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (isDesktop) RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => - Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.transactions.length, - itemBuilder: (context, index) => - Padding( - padding: const EdgeInsets.all(4), - child: DesktopTransactionCardRow( - key: Key( - "transactionCard_key_${month.transactions[index].txid}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTransactionCardRow( + key: Key( + "transactionCard_key_${month.transactions[index].txid}", + ), + transaction: + month.transactions[index], + walletId: walletId, + ), ), - transaction: - month.transactions[index], - walletId: walletId, - ), - ), ), ), if (!isDesktop) @@ -659,10 +645,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - sent: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(sent: false); setState(() {}); } }, @@ -678,10 +664,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - received: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(received: false); setState(() {}); } }, @@ -698,10 +684,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - to: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(to: null); setState(() {}); } }, @@ -717,10 +703,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - from: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(from: null); setState(() {}); } }, @@ -737,10 +723,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - amount: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(amount: null); setState(() {}); } }, @@ -756,10 +742,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - keyword: "", - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(keyword: ""); setState(() {}); } }, @@ -780,9 +766,7 @@ class _TransactionFilterOptionBarState scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox( - width: 16, - ), + separatorBuilder: (_, __) => const SizedBox(width: 16), itemBuilder: (context, index) => items[index], ), ); @@ -811,9 +795,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { borderRadius: BorderRadius.circular(1000), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - ), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -831,9 +813,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), XIcon( width: 16, height: 16, @@ -915,17 +895,23 @@ class _DesktopTransactionCardRowState localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); final coin = ref.watch(pWalletCoin(walletId)); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ) - .item1; + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; + } late final String prefix; if (Util.isDesktop) { @@ -946,8 +932,9 @@ class _DesktopTransactionCardRowState color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -970,34 +957,28 @@ class _DesktopTransactionCardRowState if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionDetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, - ), + arguments: Tuple3(_transaction, coin, walletId), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ TxIcon( @@ -1005,21 +986,16 @@ class _DesktopTransactionCardRowState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( _transaction.isCancelled ? "Cancelled" - : whatIsIt( - _transaction.type, - coin, - currentHeight, - ), - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + : whatIsIt(_transaction.type, coin, currentHeight), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -1038,20 +1014,19 @@ class _DesktopTransactionCardRowState final amount = _transaction.realAmount; return Text( "$prefix${ref.watch(pAmountFormatter(coin)).format(amount)}", - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, ), ); }, ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) Expanded( flex: 4, child: Builder( @@ -1059,11 +1034,7 @@ class _DesktopTransactionCardRowState final amount = _transaction.realAmount; return Text( - "$prefix${(amount.decimal * price).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${(amount.decimal * price!).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 11ebd1b82..edd0bff14 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -99,11 +100,12 @@ class _TransactionDetailsViewState isTokenTx = _transaction.subType == TransactionSubType.ethToken; walletId = widget.walletId; - minConfirms = ref - .read(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + minConfirms = + ref + .read(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; coin = widget.coin; amount = _transaction.realAmount; fee = _transaction.fee.toAmountAsRaw(fractionDigits: coin.fractionDigits); @@ -114,9 +116,12 @@ class _TransactionDetailsViewState amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+"; } - ethContract = isTokenTx - ? ref.read(mainDBProvider).getEthContractSync(_transaction.otherData!) - : null; + ethContract = + isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!) + : null; unit = isTokenTx ? ethContract!.symbol : coin.ticker; @@ -208,10 +213,14 @@ class _TransactionDetailsViewState return address; } try { - final contacts = ref.read(addressBookServiceProvider).contacts.where( - (element) => element.addresses - .where((element) => element.address == address) - .isNotEmpty, + final contacts = ref + .read(addressBookServiceProvider) + .contacts + .where( + (element) => + element.addresses + .where((element) => element.address == address) + .isNotEmpty, ); if (contacts.isNotEmpty) { return contacts.first.name; @@ -219,7 +228,7 @@ class _TransactionDetailsViewState return address; } } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return address; } } @@ -240,8 +249,9 @@ class _TransactionDetailsViewState builder: (_, ref, __) { return Checkbox( value: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning), + prefsChangeNotifierProvider.select( + (value) => value.hideBlockExplorerWarning, + ), ), onChanged: (value) { if (value is bool) { @@ -267,23 +277,21 @@ class _TransactionDetailsViewState child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), onPressed: () { Navigator.of(context).pop(true); }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), ), ); } else { @@ -297,10 +305,7 @@ class _TransactionDetailsViewState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Attention", - style: STextStyles.desktopH2(context), - ), + Text("Attention", style: STextStyles.desktopH2(context)), Row( children: [ Consumer( @@ -344,10 +349,7 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); + Navigator.of(context, rootNavigator: true).pop(false); }, ), const SizedBox(width: 20), @@ -356,10 +358,7 @@ class _TransactionDetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); + Navigator.of(context, rootNavigator: true).pop(true); }, ), ], @@ -378,38 +377,53 @@ class _TransactionDetailsViewState Widget build(BuildContext context) { final currentHeight = ref.watch(pWalletChainHeight(walletId)); + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.otherData!)?.value + : value.getPrice(coin)?.value, + ), + ); + } + return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : 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( - "Transaction details", - style: STextStyles.navBarTitle(context), + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : 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( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), ), - ), body: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), child: Column( children: [ if (isDesktop) @@ -425,21 +439,20 @@ class _TransactionDetailsViewState ), Expanded( child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.only(right: 32, bottom: 32) + : const EdgeInsets.all(0), child: ConditionalParent( condition: isDesktop, builder: (child) { return RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, padding: const EdgeInsets.all(0), child: child, ); @@ -447,33 +460,40 @@ class _TransactionDetailsViewState child: SingleChildScrollView( primary: isDesktop ? false : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of(context) + .extension()! + .backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants + .size + .circularBorderRadius, + ), ), - ), - ) - : null, + ) + : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -486,92 +506,59 @@ class _TransactionDetailsViewState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SelectableText( _transaction.isCancelled ? coin is Ethereum ? "Failed" : "Cancelled" : whatIsIt( - _transaction, - currentHeight, - ), + _transaction, + currentHeight, + ), style: STextStyles.desktopTextMedium( - context, - ), + context, + ), ), ], ), Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.titleBold12( - context, - ), - ), - const SizedBox( - height: 2, - ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.externalCalls, - ), - )) - SelectableText( - "$amountPrefix${(amount.decimal * ref.watch( - priceAnd24hChangeNotifierProvider - .select( - (value) => isTokenTx - ? value - .getTokenPrice( - _transaction - .otherData!, - ) - .item1 - : value - .getPrice( - coin, - ) - .item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle( + : STextStyles.titleBold12( context, ), + ), + const SizedBox(height: 2), + if (price != null) + SelectableText( + "$amountPrefix${(amount.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), ], ), @@ -589,23 +576,24 @@ class _TransactionDetailsViewState isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Status", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), ), // Flexible( // child: FittedBox( @@ -616,25 +604,30 @@ class _TransactionDetailsViewState ? coin is Ethereum ? "Failed" : "Cancelled" - : whatIsIt( - _transaction, - currentHeight, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: _transaction.type == - TransactionType.outgoing - ? Theme.of(context) - .extension()! - .accentColorOrange - : Theme.of(context) - .extension()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), + : whatIsIt(_transaction, currentHeight), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + _transaction.type == + TransactionType + .outgoing + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorOrange + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12( + context, + ), ), // ), // ), @@ -649,9 +642,7 @@ class _TransactionDetailsViewState TransactionSubType.mint)) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (!((coin is Monero || coin is Wownero) && _transaction.type == TransactionType.outgoing) && @@ -659,9 +650,10 @@ class _TransactionDetailsViewState _transaction.subType == TransactionSubType.mint)) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -687,30 +679,36 @@ class _TransactionDetailsViewState if (isDesktop) { showDialog( context: context, - builder: (_) => - DesktopDialog( - maxHeight: - double.infinity, - child: - AddressDetailsView( - addressId: - _transaction - .address - .value! - .id, - walletId: widget - .walletId, - ), - ), + builder: + ( + _, + ) => DesktopDialog( + maxHeight: + double + .infinity, + child: AddressDetailsView( + addressId: + _transaction + .address + .value! + .id, + walletId: + widget + .walletId, + ), + ), ); } else { - Navigator.of(context) - .pushNamed( + Navigator.of( + context, + ).pushNamed( AddressDetailsView .routeName, arguments: Tuple2( - _transaction.address - .value!.id, + _transaction + .address + .value! + .id, widget.walletId, ), ); @@ -725,83 +723,86 @@ class _TransactionDetailsViewState TransactionType.outgoing ? "Sent to" : "Receiving address", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), _transaction.type == TransactionType.incoming ? FutureBuilder( - future: fetchContactNameFor( - _transaction - .address.value!.value, - ), - builder: ( - builderContext, - AsyncSnapshot - snapshot, - ) { - String - addressOrContactName = - _transaction.address - .value!.value; - if (snapshot.connectionState == - ConnectionState - .done && - snapshot.hasData) { - addressOrContactName = - snapshot.data!; - } - return SelectableText( - addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + future: fetchContactNameFor( + _transaction + .address + .value! + .value, + ), + builder: ( + builderContext, + AsyncSnapshot + snapshot, + ) { + String addressOrContactName = + _transaction + .address + .value! + .value; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot.hasData) { + addressOrContactName = + snapshot.data!; + } + return SelectableText( + addressOrContactName, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), - ); - }, - ) + ); + }, + ) : SelectableText( - _transaction - .address.value!.value, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + _transaction + .address + .value! + .value, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of( - context, - ) - .extension< - StackColors>()! - .textDark, + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles - .itemSubtitle12( + : STextStyles.itemSubtitle12( context, ), - ), + ), ], ), ), @@ -815,14 +816,13 @@ class _TransactionDetailsViewState if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is Epiccash) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -835,33 +835,33 @@ class _TransactionDetailsViewState children: [ Text( "On chain note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - const SizedBox( - height: 8, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), + const SizedBox(height: 8), SelectableText( _transaction.otherData ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -875,13 +875,12 @@ class _TransactionDetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -893,104 +892,104 @@ class _TransactionDetailsViewState (coin is Epiccash) ? "Local Note" : "Note ", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), isDesktop ? IconPencilButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: _transaction.txid, - walletId: walletId, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple2( - _transaction.txid, - walletId, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension< - StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2( - context, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple2( + _transaction.txid, + walletId, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Edit", + style: STextStyles.link2( + context, ), - ], - ), + ), + ], ), + ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SelectableText( ref .watch( - pTransactionNote( - ( - txid: _transaction.txid, - walletId: walletId - ), - ), + pTransactionNote(( + txid: _transaction.txid, + walletId: walletId, + )), ) ?.value ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1002,34 +1001,36 @@ class _TransactionDetailsViewState children: [ Text( "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( Format.extractDateFrom( _transaction.timestamp, ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1038,16 +1039,21 @@ class _TransactionDetailsViewState Format.extractDateFrom( _transaction.timestamp, ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton( @@ -1061,34 +1067,36 @@ class _TransactionDetailsViewState if (coin is! NanoCurrency) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is! NanoCurrency) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Builder( builder: (context) { - final String feeString = showFeePending - ? _transaction.isConfirmed( - currentHeight, - minConfirms, - ) - ? ref + final String feeString = + showFeePending + ? _transaction.isConfirmed( + currentHeight, + minConfirms, + ) + ? ref + .watch( + pAmountFormatter(coin), + ) + .format( + fee, + withUnitName: isTokenTx, + ) + : "Pending" + : ref .watch(pAmountFormatter(coin)) .format( fee, withUnitName: isTokenTx, - ) - : "Pending" - : ref - .watch(pAmountFormatter(coin)) - .format( - fee, - withUnitName: isTokenTx, - ); + ); return Row( mainAxisAlignment: @@ -1102,55 +1110,56 @@ class _TransactionDetailsViewState children: [ Text( "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: feeString), @@ -1161,9 +1170,7 @@ class _TransactionDetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), Builder( builder: (context) { final String height; @@ -1174,10 +1181,11 @@ class _TransactionDetailsViewState if (widget.coin is Bitcoincash || widget.coin is Ecash) { - height = _transaction.height != null && - _transaction.height! > 0 - ? "${_transaction.height!}" - : "Pending"; + height = + _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; confirmations = confirms.toString(); } else if (widget.coin is Epiccash && _transaction.slateId == null) { @@ -1192,9 +1200,10 @@ class _TransactionDetailsViewState height = "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; } else { - height = confirms > 0 - ? "${_transaction.height}" - : "Pending"; + height = + confirms > 0 + ? "${_transaction.height}" + : "Pending"; } confirmations = confirms.toString(); @@ -1203,9 +1212,10 @@ class _TransactionDetailsViewState return Column( children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1218,56 +1228,58 @@ class _TransactionDetailsViewState children: [ Text( "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: height), @@ -1276,13 +1288,12 @@ class _TransactionDetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1295,56 +1306,58 @@ class _TransactionDetailsViewState children: [ Text( "Confirmations", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: height), @@ -1358,14 +1371,13 @@ class _TransactionDetailsViewState if (coin is Ethereum) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is Ethereum) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1373,25 +1385,32 @@ class _TransactionDetailsViewState children: [ Text( "Nonce", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), SelectableText( _transaction.nonce.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1399,14 +1418,13 @@ class _TransactionDetailsViewState if (kDebugMode) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (kDebugMode) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1414,38 +1432,44 @@ class _TransactionDetailsViewState children: [ Text( "Tx sub type", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), SelectableText( _transaction.subType.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1458,50 +1482,49 @@ class _TransactionDetailsViewState children: [ Text( "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - const SizedBox( - height: 8, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), + const SizedBox(height: 8), // Flexible( // child: FittedBox( // fit: BoxFit.scaleDown, // child: SelectableText( _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (coin is! Epiccash) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (coin is! Epiccash) CustomTextButton( text: "Open in block explorer", onTap: () async { final uri = getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); + coin: coin, + txid: _transaction.txid, + ); if (ref .read( @@ -1511,8 +1534,8 @@ class _TransactionDetailsViewState false) { final shouldContinue = await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); + "${uri.scheme}://${uri.host}", + ); if (!shouldContinue) { return; @@ -1527,21 +1550,22 @@ class _TransactionDetailsViewState try { await launchUrl( uri, - mode: LaunchMode - .externalApplication, + mode: + LaunchMode + .externalApplication, ); } catch (_) { - if (mounted) { + if (context.mounted) { unawaited( showDialog( context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), + builder: + (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), ); } @@ -1562,14 +1586,9 @@ class _TransactionDetailsViewState ], ), ), + if (isDesktop) const SizedBox(width: 12), if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), + IconCopyButton(data: _transaction.txid), ], ), ), @@ -1653,14 +1672,13 @@ class _TransactionDetailsViewState if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is Epiccash) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1672,14 +1690,14 @@ class _TransactionDetailsViewState children: [ Text( "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), // Flexible( // child: FittedBox( @@ -1687,27 +1705,27 @@ class _TransactionDetailsViewState // child: SelectableText( _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), // ), // ), ], ), - if (isDesktop) - const SizedBox( - width: 12, - ), + if (isDesktop) const SizedBox(width: 12), if (isDesktop) IconCopyButton( data: _transaction.slateId ?? "Unknown", @@ -1715,10 +1733,7 @@ class _TransactionDetailsViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), ], ), ), @@ -1730,101 +1745,106 @@ class _TransactionDetailsViewState ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin is Epiccash && - _transaction.getConfirmations(currentHeight) < 1 && - _transaction.isCancelled == false) - ? ConditionalParent( - condition: isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: child, - ), - child: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).extension()!.textError, + floatingActionButton: + (coin is Epiccash && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + ? ConditionalParent( + condition: isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, ), - ), - onPressed: () async { - final wallet = ref.read(pWallets).getWallet(walletId); + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).extension()!.textError, + ), + ), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + + if (wallet is EpiccashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + ), + ); + return; + } - if (wallet is EpiccashWallet) { - final String? id = _transaction.slateId; - if (id == null) { unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + showDialog( + barrierDismissible: false, context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), ), ); - return; - } - - unawaited( - showDialog( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - ), - ); - final result = - await wallet.cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { + // pop progress dialog + Navigator.of(context).pop(); - if (result.isEmpty) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - wallet.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName( - WalletView.routeName, + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + wallet.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, ), - ); - }, - ), - ); - } else { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); + ); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "ERROR: Wallet type is not Epic Cash", + context: context, + ), + ); + return; } - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - ), - ); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), ), - ), - ) - : null, + ) + : null, ), ); } @@ -1843,10 +1863,7 @@ class _Divider extends StatelessWidget { } class IconCopyButton extends StatelessWidget { - const IconCopyButton({ - super.key, - required this.data, - }); + const IconCopyButton({super.key, required this.data}); final String data; @@ -1860,9 +1877,7 @@ class IconCopyButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); if (context.mounted) { @@ -1889,10 +1904,7 @@ class IconCopyButton extends StatelessWidget { } class IconPencilButton extends StatelessWidget { - const IconPencilButton({ - super.key, - this.onPressed, - }); + const IconPencilButton({super.key, this.onPressed}); final VoidCallback? onPressed; @@ -1906,9 +1918,7 @@ class IconPencilButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () => onPressed?.call(), child: Padding( padding: const EdgeInsets.all(5), diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart index ed6e007d2..353264600 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -32,6 +33,7 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/format.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; +import '../../../../wallets/crypto_currency/coins/ethereum.dart'; import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; @@ -51,11 +53,8 @@ import '../transaction_search_filter_view.dart'; import 'transaction_v2_card.dart'; import 'transaction_v2_details_view.dart'; -typedef _GroupedTransactions = ({ - String label, - DateTime startDate, - List transactions -}); +typedef _GroupedTransactions = + ({String label, DateTime startDate, List transactions}); class AllTransactionsV2View extends ConsumerStatefulWidget { const AllTransactionsV2View({ @@ -108,13 +107,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { // debugPrint("FILTER: $filter"); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions.where((tx) { if (!filter.sent && !filter.received) { @@ -160,23 +160,25 @@ class _AllTransactionsV2ViewState extends ConsumerState { bool contains = false; // check if address book name contains - contains |= contacts - .where( - (e) => - e.addresses - .map((e) => e.address) - .toSet() - .intersection(tx.associatedAddresses()) - .isNotEmpty && - e.name.toLowerCase().contains(keyword), - ) - .isNotEmpty; + contains |= + contacts + .where( + (e) => + e.addresses + .map((e) => e.address) + .toSet() + .intersection(tx.associatedAddresses()) + .isNotEmpty && + e.name.toLowerCase().contains(keyword), + ) + .isNotEmpty; // check if address contains - contains |= tx - .associatedAddresses() - .where((e) => e.toLowerCase().contains(keyword)) - .isNotEmpty; + contains |= + tx + .associatedAddresses() + .where((e) => e.toLowerCase().contains(keyword)) + .isNotEmpty; TransactionNote? note; final matchingNotes = notes.where((e) => e.txid == tx.txid); @@ -197,8 +199,9 @@ class _AllTransactionsV2ViewState extends ConsumerState { contains |= tx.type.name.toLowerCase().contains(keyword); // check if date contains - contains |= - Format.extractDateFrom(tx.timestamp).toLowerCase().contains(keyword); + contains |= Format.extractDateFrom( + tx.timestamp, + ).toLowerCase().contains(keyword); return contains; } @@ -212,13 +215,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { } text = text.toLowerCase(); final contacts = ref.read(addressBookServiceProvider).contacts; - final notes = ref - .read(mainDBProvider) - .isar - .transactionNotes - .where() - .walletIdEqualTo(walletId) - .findAllSync(); + final notes = + ref + .read(mainDBProvider) + .isar + .transactionNotes + .where() + .walletIdEqualTo(walletId) + .findAllSync(); return transactions .where((tx) => _isKeywordMatch(tx, text, contacts, notes)) @@ -234,8 +238,11 @@ class _AllTransactionsV2ViewState extends ConsumerState { final date = DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000); final monthYear = "${Constants.monthMap[date.month]} ${date.year}"; if (map[monthYear] == null) { - map[monthYear] = - (label: monthYear, startDate: date, transactions: [tx]); + map[monthYear] = ( + label: monthYear, + startDate: date, + transactions: [tx], + ); } else { map[monthYear]!.transactions.add(tx); } @@ -252,96 +259,94 @@ class _AllTransactionsV2ViewState extends ConsumerState { return MasterScaffold( background: Theme.of(context).extension()!.background, isDesktop: isDesktop, - appBar: isDesktop - ? DesktopAppBar( - isCompactHeight: true, - background: Theme.of(context).extension()!.popupBG, - leading: Row( - children: [ - const SizedBox( - width: 32, - ), - 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, - ), - const SizedBox( - width: 12, - ), - Text( - "Transactions", - style: STextStyles.desktopH3(context), - ), - ], - ), - ) - : AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: AppBarBackButton( - onPressed: () async { - if (FocusScope.of(context).hasFocus) { - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 75), - ); - } - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - ), - title: Text( - "Transactions", - style: STextStyles.navBarTitle(context), - ), - actions: [ - Padding( - padding: const EdgeInsets.only( - top: 10, - bottom: 10, - right: 20, - ), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - key: const Key("transactionSearchFilterViewButton"), - size: 36, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + const SizedBox(width: 32), + AppBarIconButton( + size: 32, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], - color: Theme.of(context) - .extension()! - .background, icon: SvgPicture.asset( - Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, - width: 20, - height: 20, + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), - onPressed: () { - Navigator.of(context).pushNamed( - TransactionSearchFilterView.routeName, - arguments: ref.read(pWalletCoin(walletId)), - ); - }, + onPressed: Navigator.of(context).pop, ), - ), + const SizedBox(width: 12), + Text("Transactions", style: STextStyles.desktopH3(context)), + ], ), - ], - ), + ) + : AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75), + ); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Transactions", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("transactionSearchFilterViewButton"), + size: 36, + shadows: const [], + color: + Theme.of( + context, + ).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.filter, + color: + Theme.of( + context, + ).extension()!.accentColorDark, + width: 20, + height: 20, + ), + onPressed: () { + Navigator.of(context).pushNamed( + TransactionSearchFilterView.routeName, + arguments: ref.read(pWalletCoin(walletId)), + ); + }, + ), + ), + ), + ], + ), body: Padding( padding: EdgeInsets.only( left: isDesktop ? 20 : 12, @@ -356,15 +361,10 @@ class _AllTransactionsV2ViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => SizedBox( - width: 570, - child: child, - ), + builder: (child) => SizedBox(width: 570, child: child), child: ConditionalParent( condition: !isDesktop, - builder: (child) => Expanded( - child: child, - ), + builder: (child) => Expanded(child: child), child: ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -379,15 +379,18 @@ class _AllTransactionsV2ViewState extends ConsumerState { _searchString = value; }); }, - style: isDesktop - ? STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - height: 1.8, - ) - : STextStyles.field(context), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), decoration: standardInputDecoration( "Search...", searchFieldFocusNode, @@ -405,35 +408,33 @@ class _AllTransactionsV2ViewState extends ConsumerState { height: isDesktop ? 20 : 16, ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), ), - if (isDesktop) - const SizedBox( - width: 20, - ), + if (isDesktop) const SizedBox(width: 20), if (isDesktop) SecondaryButton( buttonHeight: ButtonHeight.l, @@ -441,9 +442,10 @@ class _AllTransactionsV2ViewState extends ConsumerState { label: "Filter", icon: SvgPicture.asset( Assets.svg.filter, - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, width: 20, height: 20, ), @@ -468,25 +470,14 @@ class _AllTransactionsV2ViewState extends ConsumerState { ], ), ), - if (isDesktop) - const SizedBox( - height: 8, - ), + if (isDesktop) const SizedBox(height: 8), if (isDesktop && ref.watch(transactionFilterProvider.state).state != null) const Padding( - padding: EdgeInsets.symmetric( - vertical: 8, - ), - child: Row( - children: [ - TransactionFilterOptionBar(), - ], - ), + padding: EdgeInsets.symmetric(vertical: 8), + child: Row(children: [TransactionFilterOptionBar()]), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -494,33 +485,35 @@ class _AllTransactionsV2ViewState extends ConsumerState { ref.watch(transactionFilterProvider.state).state; return FutureBuilder( - future: ref - .watch(mainDBProvider) - .isar - .transactionV2s - .buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [widget.walletId], - ), - ], - filter: widget.contractAddress == null - ? ref - .watch(pWallets) - .getWallet(widget.walletId) - .transactionFilterOperation - : ref - .read(pCurrentTokenWallet)! - .transactionFilterOperation, - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, - ), - ], - ) - .findAll(), + future: + ref + .watch(mainDBProvider) + .isar + .transactionV2s + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo( + indexName: 'walletId', + value: [widget.walletId], + ), + ], + filter: + widget.contractAddress == null + ? ref + .watch(pWallets) + .getWallet(widget.walletId) + .transactionFilterOperation + : ref + .read(pCurrentTokenWallet)! + .transactionFilterOperation, + sortBy: [ + const SortProperty( + property: "timestamp", + sort: Sort.desc, + ), + ], + ) + .findAll(), builder: (_, AsyncSnapshot> snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -549,43 +542,39 @@ class _AllTransactionsV2ViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.label, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), if (isDesktop) RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => - Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.transactions.length, - itemBuilder: (context, index) => - Padding( - padding: const EdgeInsets.all(4), - child: DesktopTransactionCardRow( - key: Key( - "transactionCard_key_${month.transactions[index].txid}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTransactionCardRow( + key: Key( + "transactionCard_key_${month.transactions[index].txid}", + ), + transaction: + month.transactions[index], + walletId: walletId, + ), ), - transaction: - month.transactions[index], - walletId: walletId, - ), - ), ), ), if (!isDesktop) @@ -652,10 +641,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - sent: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(sent: false); setState(() {}); } }, @@ -671,10 +660,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - received: false, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(received: false); setState(() {}); } }, @@ -691,10 +680,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - to: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(to: null); setState(() {}); } }, @@ -710,10 +699,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - from: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(from: null); setState(() {}); } }, @@ -730,10 +719,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - amount: null, - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(amount: null); setState(() {}); } }, @@ -749,10 +738,10 @@ class _TransactionFilterOptionBarState if (items.isEmpty) { ref.read(transactionFilterProvider.state).state = null; } else { - ref.read(transactionFilterProvider.state).state = - ref.read(transactionFilterProvider.state).state?.copyWith( - keyword: "", - ); + ref.read(transactionFilterProvider.state).state = ref + .read(transactionFilterProvider.state) + .state + ?.copyWith(keyword: ""); setState(() {}); } }, @@ -773,9 +762,7 @@ class _TransactionFilterOptionBarState scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: items.length, - separatorBuilder: (_, __) => const SizedBox( - width: 16, - ), + separatorBuilder: (_, __) => const SizedBox(width: 16), itemBuilder: (context, index) => items[index], ), ); @@ -804,9 +791,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { borderRadius: BorderRadius.circular(1000), ), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - ), + padding: const EdgeInsets.symmetric(horizontal: 14), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -824,9 +809,7 @@ class TransactionFilterOptionBarItem extends StatelessWidget { ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), XIcon( width: 16, height: 16, @@ -865,23 +848,25 @@ class _DesktopTransactionCardRowState bool get isTokenTx => ethContract != null; String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( - currentChainHeight: height, - minConfirms: minConfirms, - minCoinbaseConfirms: ref + currentChainHeight: height, + minConfirms: minConfirms, + minCoinbaseConfirms: + ref .read(pWallets) .getWallet(widget.walletId) .cryptoCurrency .minCoinbaseConfirms, - ); + ); @override void initState() { walletId = widget.walletId; - minConfirms = ref - .read(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; + minConfirms = + ref + .read(pWallets) + .getWallet(widget.walletId) + .cryptoCurrency + .minConfirms; _transaction = widget.transaction; if (_transaction.subType == TransactionSubType.ethToken) { @@ -901,17 +886,25 @@ class _DesktopTransactionCardRowState localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); final coin = ref.watch(pWalletCoin(walletId)); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ) - .item1; + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.contractAddress!)?.value + : value.getPrice(coin)?.value, + ), + ); + } late final String prefix; if (Util.isDesktop) { @@ -939,6 +932,7 @@ class _DesktopTransactionCardRowState case TransactionType.outgoing: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -970,6 +964,7 @@ class _DesktopTransactionCardRowState case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; } @@ -979,8 +974,9 @@ class _DesktopTransactionCardRowState color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -992,34 +988,28 @@ class _DesktopTransactionCardRowState if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionV2DetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionV2DetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionV2DetailsView.routeName, - arguments: ( - tx: _transaction, - coin: coin, - walletId: walletId, - ), + arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ TxIcon( @@ -1027,18 +1017,14 @@ class _DesktopTransactionCardRowState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( - whatIsIt( - _transaction, - currentHeight, - ), - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + whatIsIt(_transaction, currentHeight), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -1062,24 +1048,18 @@ class _DesktopTransactionCardRowState flex: 6, child: Text( "$prefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) Expanded( flex: 4, child: Text( - "$prefix${(amount.decimal * price).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${(amount.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ), ), diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart index 34abbb43e..5219b3f26 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/boost_transaction_view.dart @@ -23,6 +23,7 @@ import '../../../../utilities/amount/amount_formatter.dart'; import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; +import '../../../../wallets/crypto_currency/coins/ethereum.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../widgets/background.dart'; @@ -37,10 +38,7 @@ import '../../../../widgets/stack_dialog.dart'; import '../../../send_view/confirm_transaction_view.dart'; class BoostTransactionView extends ConsumerStatefulWidget { - const BoostTransactionView({ - super.key, - required this.transaction, - }); + const BoostTransactionView({super.key, required this.transaction}); static const String routeName = "/boostTransaction"; @@ -73,10 +71,11 @@ class _BoostTransactionViewState extends ConsumerState { if (_newRate <= rate) { await showDialog( context: context, - builder: (_) => const StackOkDialog( - title: "Error", - message: "New fee rate must be greater than the current rate.", - ), + builder: + (_) => const StackOkDialog( + title: "Error", + message: "New fee rate must be greater than the current rate.", + ), ); return; } @@ -99,11 +98,12 @@ class _BoostTransactionViewState extends ConsumerState { if (txData == null && mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "RBF send error", - message: ex?.toString() ?? "Unknown error found", - maxWidth: 600, - ), + builder: + (_) => StackOkDialog( + title: "RBF send error", + message: ex?.toString() ?? "Unknown error found", + maxWidth: 600, + ), ); return; } else { @@ -112,17 +112,18 @@ class _BoostTransactionViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmTransactionView( - txData: txData!, - walletId: walletId, - onSuccess: () {}, - // isPaynymTransaction: isPaynymSend, TODO ? - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData!, + walletId: walletId, + onSuccess: () {}, + // isPaynymTransaction: isPaynymSend, TODO ? + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } else if (mounted) { @@ -130,12 +131,13 @@ class _BoostTransactionViewState extends ConsumerState { Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: txData!, - walletId: walletId, - // isPaynymTransaction: isPaynymSend, TODO ? - onSuccess: () {}, - ), + builder: + (_) => ConfirmTransactionView( + txData: txData!, + walletId: walletId, + // isPaynymTransaction: isPaynymSend, TODO ? + onSuccess: () {}, + ), settings: const RouteSettings( name: ConfirmTransactionView.routeName, ), @@ -159,6 +161,7 @@ class _BoostTransactionViewState extends ConsumerState { ); amount = _transaction.getAmountSentFromThisWallet( fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, + subtractFee: ref.read(pWalletCoin(walletId)) is! Ethereum, ); rate = (fee.raw ~/ BigInt.from(_transaction.vSize!)).toInt(); _newRate = rate + 1; @@ -169,61 +172,56 @@ class _BoostTransactionViewState extends ConsumerState { @override Widget build(BuildContext context) { final coin = ref.watch(pWalletCoin(walletId)); - final String feeString = ref.watch(pAmountFormatter(coin)).format( - fee, - ); - final String amountString = ref.watch(pAmountFormatter(coin)).format( - amount, - ); + final String feeString = ref.watch(pAmountFormatter(coin)).format(fee); + final String amountString = ref + .watch(pAmountFormatter(coin)) + .format(amount); final String feeRateString = "$rate sats/vByte"; 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 { - Navigator.of(context).pop(); - }, - ), - title: Text( - "Boost transaction", - style: STextStyles.navBarTitle(context), + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Boost transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: child, ), ), - body: child, - ), - ), child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.only(left: 32, right: 32, bottom: 32) + : const EdgeInsets.all(12), child: ConditionalParent( condition: isDesktop, builder: (child) { return Column( children: [ RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, padding: const EdgeInsets.all(0), child: child, ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", @@ -238,10 +236,11 @@ class _BoostTransactionViewState extends ConsumerState { children: [ ConditionalParent( condition: isDesktop, - builder: (child) => RoundedWhiteContainer( - padding: EdgeInsets.zero, - child: child, - ), + builder: + (child) => RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: child, + ), child: Column( children: [ DetailItem( @@ -277,15 +276,9 @@ class _BoostTransactionViewState extends ConsumerState { ), ), if (!isDesktop) const Spacer(), + if (!isDesktop) const SizedBox(height: 16), if (!isDesktop) - const SizedBox( - height: 16, - ), - if (!isDesktop) - PrimaryButton( - label: "Preview send", - onPressed: _previewTxn, - ), + PrimaryButton(label: "Preview send", onPressed: _previewTxn), ], ), ), @@ -305,9 +298,7 @@ class _Divider extends StatelessWidget { color: Theme.of(context).extension()!.backgroundAppBar, ); } else { - return const SizedBox( - height: 12, - ); + return const SizedBox(height: 12); } } } diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart index 34e5c662b..9af007aef 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_card.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -25,10 +26,7 @@ import '../../sub_widgets/tx_icon.dart'; import 'transaction_v2_details_view.dart'; class TransactionCardV2 extends ConsumerStatefulWidget { - const TransactionCardV2({ - super.key, - required this.transaction, - }); + const TransactionCardV2({super.key, required this.transaction}); final TransactionV2 transaction; @@ -47,25 +45,24 @@ class _TransactionCardStateV2 extends ConsumerState { bool get isTokenTx => tokenContract != null; - String whatIsIt( - CryptoCurrency coin, - int currentHeight, - ) => + String whatIsIt(CryptoCurrency coin, int currentHeight) => _transaction.isCancelled && coin is Ethereum ? "Failed" : _transaction.statusLabel( - currentChainHeight: currentHeight, - minConfirms: ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .minConfirms, - minCoinbaseConfirms: ref - .read(pWallets) - .getWallet(walletId) - .cryptoCurrency - .minCoinbaseConfirms, - ); + currentChainHeight: currentHeight, + minConfirms: + ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .minConfirms, + minCoinbaseConfirms: + ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .minCoinbaseConfirms, + ); @override void initState() { @@ -106,18 +103,23 @@ class _TransactionCardStateV2 extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => isTokenTx - ? value.getTokenPrice(tokenContract!.address) - : value.getPrice(coin), - ), - ) - .item1; + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(tokenContract!.address)?.value + : value.getPrice(coin)?.value, + ), + ); + } final currentHeight = ref.watch(pWalletChainHeight(walletId)); @@ -134,6 +136,7 @@ class _TransactionCardStateV2 extends ConsumerState { case TransactionType.outgoing: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -165,6 +168,7 @@ class _TransactionCardStateV2 extends ConsumerState { case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; } @@ -174,8 +178,9 @@ class _TransactionCardStateV2 extends ConsumerState { color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: Padding( padding: const EdgeInsets.all(6), @@ -189,25 +194,22 @@ class _TransactionCardStateV2 extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionV2DetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionV2DetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionV2DetailsView.routeName, - arguments: ( - tx: _transaction, - coin: coin, - walletId: walletId, - ), + arguments: (tx: _transaction, coin: coin, walletId: walletId), ), ); } @@ -221,9 +223,7 @@ class _TransactionCardStateV2 extends ConsumerState { coin: coin, currentHeight: currentHeight, ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -236,17 +236,12 @@ class _TransactionCardStateV2 extends ConsumerState { child: FittedBox( fit: BoxFit.scaleDown, child: Text( - whatIsIt( - coin, - currentHeight, - ), + whatIsIt(coin, currentHeight), style: STextStyles.itemSubtitle12(context), ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -262,9 +257,7 @@ class _TransactionCardStateV2 extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.end, @@ -278,29 +271,15 @@ class _TransactionCardStateV2 extends ConsumerState { ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) - const SizedBox( - width: 10, - ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null) const SizedBox(width: 10), + if (price != null) Flexible( child: FittedBox( fit: BoxFit.scaleDown, child: Builder( builder: (_) { return Text( - "$prefix${Amount.fromDecimal( - amount.decimal * price, - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${Amount.fromDecimal(amount.decimal * price!, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.label(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index 8f0852941..06469c92b 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -10,6 +10,7 @@ import 'dart:async'; +import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -133,43 +134,43 @@ class _TransactionV2DetailsViewState if (mounted) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Boost transaction", - style: STextStyles.desktopH3(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Boost transaction", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: SingleChildScrollView( + child: BoostTransactionView( + transaction: _transaction, + ), ), ), - const DesktopDialogCloseButton(), ], ), - Flexible( - child: SingleChildScrollView( - child: BoostTransactionView( - transaction: _transaction, - ), - ), - ), - ], - ), - ), + ), ); } } else { unawaited( - Navigator.of(context).pushNamed( - BoostTransactionView.routeName, - arguments: _transaction, - ), + Navigator.of( + context, + ).pushNamed(BoostTransactionView.routeName, arguments: _transaction), ); } } finally { @@ -185,13 +186,15 @@ class _TransactionV2DetailsViewState final wallet = ref.read(pWallets).getWallet(walletId); - hasTxKeyProbably = wallet is LibMoneroWallet && + hasTxKeyProbably = + wallet is LibMoneroWallet && (_transaction.type == TransactionType.outgoing || _transaction.type == TransactionType.sentToSelf); if (_transaction.type case TransactionType.sentToSelf || TransactionType.outgoing) { - supportsRbf = _transaction.subType == TransactionSubType.none && + supportsRbf = + _transaction.subType == TransactionSubType.none && wallet is RbfInterface; } else { supportsRbf = false; @@ -230,6 +233,7 @@ class _TransactionV2DetailsViewState case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); break; @@ -240,81 +244,86 @@ class _TransactionV2DetailsViewState ); break; } - data = _transaction.outputs - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.outputs + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); } else if (_transaction.subType == TransactionSubType.cashFusion) { amount = _transaction.getAmountReceivedInThisWallet( fractionDigits: fractionDigits, ); - data = _transaction.outputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, - ) - ), - ) - .toList(); - } else { - switch (_transaction.type) { - case TransactionType.outgoing: - amount = _transaction.getAmountSentFromThisWallet( - fractionDigits: fractionDigits, - ); - data = _transaction.outputs - .where((e) => !e.walletOwns) + data = + _transaction.outputs + .where((e) => e.walletOwns) .map( (e) => ( addresses: e.addresses, amount: Amount( rawValue: e.value, fractionDigits: coin.fractionDigits, - ) + ), ), ) .toList(); + } else { + switch (_transaction.type) { + case TransactionType.outgoing: + amount = _transaction.getAmountSentFromThisWallet( + fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, + ); + data = + _transaction.outputs + .where((e) => !e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), + ) + .toList(); break; case TransactionType.incoming: case TransactionType.sentToSelf: if (_transaction.subType == TransactionSubType.sparkMint || _transaction.subType == TransactionSubType.sparkSpend) { - _sparkMemo = ref - .read(mainDBProvider) - .isar - .sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .filter() - .memoIsNotEmpty() - .and() - .heightEqualTo(_transaction.height) - .anyOf( - _transaction.outputs - .where( - (e) => - e.walletOwns && - e.addresses.isEmpty && - e.scriptPubKeyHex.length >= 488, - ) - .map((e) => e.scriptPubKeyHex.substring(2, 488)) - .toList(), - (q, element) => q.serializedCoinB64StartsWith(element), - ) - .memoProperty() - .findFirstSync(); + _sparkMemo = + ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .memoIsNotEmpty() + .and() + .heightEqualTo(_transaction.height) + .anyOf( + _transaction.outputs + .where( + (e) => + e.walletOwns && + e.addresses.isEmpty && + e.scriptPubKeyHex.length >= 488, + ) + .map((e) => e.scriptPubKeyHex.substring(2, 488)) + .toList(), + (q, element) => q.serializedCoinB64StartsWith(element), + ) + .memoProperty() + .findFirstSync(); } if (_transaction.subType == TransactionSubType.sparkMint) { @@ -338,36 +347,39 @@ class _TransactionV2DetailsViewState fractionDigits: fractionDigits, ); } - data = _transaction.outputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.outputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); break; case TransactionType.unknown: amount = _transaction.getAmountSentFromThisWallet( fractionDigits: fractionDigits, + subtractFee: coin is! Ethereum, ); - data = _transaction.inputs - .where((e) => e.walletOwns) - .map( - (e) => ( - addresses: e.addresses, - amount: Amount( - rawValue: e.value, - fractionDigits: coin.fractionDigits, + data = + _transaction.inputs + .where((e) => e.walletOwns) + .map( + (e) => ( + addresses: e.addresses, + amount: Amount( + rawValue: e.value, + fractionDigits: coin.fractionDigits, + ), + ), ) - ), - ) - .toList(); + .toList(); break; } } @@ -381,24 +393,29 @@ class _TransactionV2DetailsViewState } String whatIsIt(TransactionV2 tx, int height) => tx.statusLabel( - currentChainHeight: height, - minConfirms: minConfirms, - minCoinbaseConfirms: ref + currentChainHeight: height, + minConfirms: minConfirms, + minCoinbaseConfirms: + ref .read(pWallets) .getWallet(walletId) .cryptoCurrency .minCoinbaseConfirms, - ); + ); Future fetchContactNameFor(String address) async { if (address.isEmpty) { return address; } try { - final contacts = ref.read(addressBookServiceProvider).contacts.where( - (element) => element.addresses - .where((element) => element.address == address) - .isNotEmpty, + final contacts = ref + .read(addressBookServiceProvider) + .contacts + .where( + (element) => + element.addresses + .where((element) => element.address == address) + .isNotEmpty, ); if (contacts.isNotEmpty) { return contacts.first.name; @@ -406,7 +423,7 @@ class _TransactionV2DetailsViewState return address; } } catch (e, s) { - Logging.instance.w("$e\n$s", error: e, stackTrace: s,); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); return address; } } @@ -427,8 +444,9 @@ class _TransactionV2DetailsViewState builder: (_, ref, __) { return Checkbox( value: ref.watch( - prefsChangeNotifierProvider - .select((value) => value.hideBlockExplorerWarning), + prefsChangeNotifierProvider.select( + (value) => value.hideBlockExplorerWarning, + ), ), onChanged: (value) { if (value is bool) { @@ -454,23 +472,21 @@ class _TransactionV2DetailsViewState child: Text( "Cancel", style: STextStyles.button(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorDark, + color: + Theme.of( + context, + ).extension()!.accentColorDark, ), ), ), rightButton: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), + style: Theme.of( + context, + ).extension()!.getPrimaryEnabledButtonStyle(context), onPressed: () { Navigator.of(context).pop(true); }, - child: Text( - "Continue", - style: STextStyles.button(context), - ), + child: Text("Continue", style: STextStyles.button(context)), ), ); } else { @@ -484,10 +500,7 @@ class _TransactionV2DetailsViewState Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Attention", - style: STextStyles.desktopH2(context), - ), + Text("Attention", style: STextStyles.desktopH2(context)), Row( children: [ Consumer( @@ -531,10 +544,7 @@ class _TransactionV2DetailsViewState buttonHeight: ButtonHeight.l, label: "Cancel", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(false); + Navigator.of(context, rootNavigator: true).pop(false); }, ), const SizedBox(width: 20), @@ -543,10 +553,7 @@ class _TransactionV2DetailsViewState buttonHeight: ButtonHeight.l, label: "Continue", onPressed: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(true); + Navigator.of(context, rootNavigator: true).pop(true); }, ), ], @@ -585,38 +592,53 @@ class _TransactionV2DetailsViewState coin.minCoinbaseConfirms, ); + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.contractAddress!)?.value + : value.getPrice(coin)?.value, + ), + ); + } + return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: child, - ), + builder: (child) => Background(child: child), child: Scaffold( - backgroundColor: isDesktop - ? Colors.transparent - : Theme.of(context).extension()!.background, - appBar: isDesktop - ? null - : 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( - "Transaction details", - style: STextStyles.navBarTitle(context), + backgroundColor: + isDesktop + ? Colors.transparent + : Theme.of(context).extension()!.background, + appBar: + isDesktop + ? null + : 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( + "Transaction details", + style: STextStyles.navBarTitle(context), + ), ), - ), body: Padding( - padding: isDesktop - ? const EdgeInsets.only(left: 32) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.only(left: 32) + : const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -633,21 +655,20 @@ class _TransactionV2DetailsViewState ), Flexible( child: Padding( - padding: isDesktop - ? const EdgeInsets.only( - right: 32, - bottom: 32, - ) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.only(right: 32, bottom: 32) + : const EdgeInsets.all(0), child: ConditionalParent( condition: isDesktop, builder: (child) { return RoundedWhiteContainer( - borderColor: isDesktop - ? Theme.of(context) - .extension()! - .backgroundAppBar - : null, + borderColor: + isDesktop + ? Theme.of( + context, + ).extension()!.backgroundAppBar + : null, padding: const EdgeInsets.all(0), child: child, ); @@ -655,34 +676,41 @@ class _TransactionV2DetailsViewState child: SingleChildScrollView( primary: isDesktop ? false : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(4), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(4), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(12), child: Container( - decoration: isDesktop - ? BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + decoration: + isDesktop + ? BoxDecoration( + color: + Theme.of(context) + .extension()! + .backgroundAppBar, + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants + .size + .circularBorderRadius, + ), ), - ), - ) - : null, + ) + : null, child: Padding( - padding: isDesktop - ? const EdgeInsets.all(12) - : const EdgeInsets.all(0), + padding: + isDesktop + ? const EdgeInsets.all(12) + : const EdgeInsets.all(0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -695,9 +723,7 @@ class _TransactionV2DetailsViewState currentHeight: currentHeight, coin: coin, ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SelectableText( whatIsIt( _transaction, @@ -705,71 +731,72 @@ class _TransactionV2DetailsViewState ), style: STextStyles.desktopTextMedium( - context, - ), + context, + ), ), ], ), Column( - crossAxisAlignment: isDesktop - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, + crossAxisAlignment: + isDesktop + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ SelectableText( "$amountPrefix${ref.watch(pAmountFormatter(coin)).format(amount, ethContract: ethContract)}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.titleBold12( - context, - ), - ), - const SizedBox( - height: 2, - ), - if (ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.externalCalls, - ), - )) - SelectableText( - "$amountPrefix${(amount.decimal * ref.watch( - priceAnd24hChangeNotifierProvider - .select( - (value) => value - .getPrice( - coin, - ) - .item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider - .select( - (value) => value.currency, - ), - )}", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, ) - : STextStyles.itemSubtitle( + : STextStyles.titleBold12( context, ), + ), + const SizedBox(height: 2), + if (price != null) + Builder( + builder: (context) { + final total = (amount.decimal * + price!) + .toAmount( + fractionDigits: 2, + ); + final formatted = total.fiatString( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => + value.locale, + ), + ), + ); + final ticker = ref.watch( + prefsChangeNotifierProvider + .select( + (value) => + value.currency, + ), + ); + return SelectableText( + "$amountPrefix$formatted $ticker", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ); + }, ), ], ), @@ -787,52 +814,58 @@ class _TransactionV2DetailsViewState isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Status", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle(context), ), // Flexible( // child: FittedBox( // fit: BoxFit.scaleDown, // child: SelectableText( - whatIsIt( - _transaction, - currentHeight, - ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: _transaction.type == - TransactionType - .outgoing && - _transaction.subType != - TransactionSubType - .cashFusion - ? Theme.of(context) - .extension()! - .accentColorOrange - : Theme.of(context) - .extension()! - .accentColorGreen, - ) - : STextStyles.itemSubtitle12(context), + whatIsIt(_transaction, currentHeight), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + _transaction.type == + TransactionType + .outgoing && + _transaction + .subType != + TransactionSubType + .cashFusion + ? Theme.of(context) + .extension< + StackColors + >()! + .accentColorOrange + : Theme.of(context) + .extension< + StackColors + >()! + .accentColorGreen, + ) + : STextStyles.itemSubtitle12( + context, + ), ), // ), // ), @@ -847,9 +880,7 @@ class _TransactionV2DetailsViewState TransactionSubType.mint)) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (!((coin is Monero || coin is Wownero) && _transaction.type == TransactionType.outgoing) && @@ -857,9 +888,10 @@ class _TransactionV2DetailsViewState _transaction.subType == TransactionSubType.mint)) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -925,19 +957,17 @@ class _TransactionV2DetailsViewState }, child: Text( outputLabel, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -952,11 +982,13 @@ class _TransactionV2DetailsViewState builder: ( builderContext, AsyncSnapshot - snapshot, + snapshot, ) { String - addressOrContactName = - data.first.addresses + addressOrContactName = + data + .first + .addresses .first; if (snapshot.connectionState == ConnectionState @@ -967,92 +999,95 @@ class _TransactionV2DetailsViewState } return SelectableText( addressOrContactName, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, ) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + : STextStyles.itemSubtitle12( + context, + ), ); }, ) else - for (int i = 0; - i < data.length; - i++) + for ( + int i = 0; + i < data.length; + i++ + ) ConditionalParent( condition: i > 0, - builder: (child) => Column( - crossAxisAlignment: - CrossAxisAlignment - .stretch, - children: [ - const _Divider(), - child, - ], - ), + builder: + (child) => Column( + crossAxisAlignment: + CrossAxisAlignment + .stretch, + children: [ + const _Divider(), + child, + ], + ), child: Padding( padding: const EdgeInsets.all( - 8.0, - ), + 8.0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment .start, children: [ - ...data[i] - .addresses - .map( - (e) { - return FutureBuilder( - future: - fetchContactNameFor( - e, - ), - builder: ( - builderContext, - AsyncSnapshot< - String> - snapshot, - ) { - final String - addressOrContactName; - if (snapshot.connectionState == - ConnectionState - .done && + ...data[i].addresses.map(( + e, + ) { + return FutureBuilder( + future: + fetchContactNameFor( + e, + ), + builder: ( + builderContext, + AsyncSnapshot< + String + > + snapshot, + ) { + final String + addressOrContactName; + if (snapshot.connectionState == + ConnectionState + .done && + snapshot + .hasData) { + addressOrContactName = snapshot - .hasData) { - addressOrContactName = - snapshot - .data!; - } else { - addressOrContactName = - e; - } - - return OutputCard( - address: - addressOrContactName, - amount: data[ - i] - .amount, - coin: coin, - ); - }, - ); - }, - ), + .data!; + } else { + addressOrContactName = + e; + } + + return OutputCard( + address: + addressOrContactName, + amount: + data[i] + .amount, + coin: coin, + ); + }, + ); + }), ], ), ), @@ -1072,14 +1107,13 @@ class _TransactionV2DetailsViewState if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is Epiccash) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1092,33 +1126,33 @@ class _TransactionV2DetailsViewState children: [ Text( "On chain note", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), - const SizedBox( - height: 8, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), + const SizedBox(height: 8), SelectableText( _transaction.onChainNote ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1132,13 +1166,12 @@ class _TransactionV2DetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1150,94 +1183,96 @@ class _TransactionV2DetailsViewState (coin is Epiccash) ? "Local Note" : "Note ", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), isDesktop ? IconPencilButton( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return DesktopDialog( - maxWidth: 580, - maxHeight: 360, - child: EditNoteView( - txid: _transaction.txid, - walletId: walletId, - ), - ); - }, - ); - }, - ) - : GestureDetector( - onTap: () { - Navigator.of(context).pushNamed( - EditNoteView.routeName, - arguments: Tuple2( - _transaction.txid, - walletId, - ), - ); - }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.pencil, - width: 10, - height: 10, - color: Theme.of(context) - .extension< - StackColors>()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Edit", - style: STextStyles.link2( - context, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 580, + maxHeight: 360, + child: EditNoteView( + txid: _transaction.txid, + walletId: walletId, ), + ); + }, + ); + }, + ) + : GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + EditNoteView.routeName, + arguments: Tuple2( + _transaction.txid, + walletId, + ), + ); + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.pencil, + width: 10, + height: 10, + color: + Theme.of(context) + .extension< + StackColors + >()! + .infoItemIcons, + ), + const SizedBox(width: 4), + Text( + "Edit", + style: STextStyles.link2( + context, ), - ], - ), + ), + ], ), + ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SelectableText( ref .watch( - pTransactionNote( - ( - txid: (coin is Epiccash) - ? _transaction.slateId - .toString() - : _transaction.txid, - walletId: walletId - ), - ), + pTransactionNote(( + txid: + (coin is Epiccash) + ? _transaction.slateId + .toString() + : _transaction.txid, + walletId: walletId, + )), ) ?.value ?? "", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1245,14 +1280,13 @@ class _TransactionV2DetailsViewState if (_sparkMemo != null) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (_sparkMemo != null) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1260,45 +1294,47 @@ class _TransactionV2DetailsViewState children: [ Text( "Memo", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), SelectableText( _sparkMemo!, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1310,34 +1346,36 @@ class _TransactionV2DetailsViewState children: [ Text( "Date", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - if (isDesktop) - const SizedBox( - height: 2, - ), + if (isDesktop) const SizedBox(height: 2), if (isDesktop) SelectableText( Format.extractDateFrom( _transaction.timestamp, ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1346,16 +1384,21 @@ class _TransactionV2DetailsViewState Format.extractDateFrom( _transaction.timestamp, ), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton( @@ -1367,39 +1410,39 @@ class _TransactionV2DetailsViewState ), ), if (coin is! NanoCurrency && - !(coin is Xelis && _transaction.type == TransactionType.incoming) - ) + !(coin is Xelis && + _transaction.type == + TransactionType.incoming)) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is! NanoCurrency && - !(coin is Xelis && _transaction.type == TransactionType.incoming) - ) + !(coin is Xelis && + _transaction.type == + TransactionType.incoming)) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Builder( builder: (context) { - final String feeString = showFeePending - ? _transaction.isConfirmed( - currentHeight, - minConfirms, - coin.minCoinbaseConfirms, - ) - ? ref - .watch(pAmountFormatter(coin)) - .format( - fee, + final String feeString = + showFeePending + ? _transaction.isConfirmed( + currentHeight, + minConfirms, + coin.minCoinbaseConfirms, ) - : "Pending" - : ref - .watch(pAmountFormatter(coin)) - .format( - fee, - ); + ? ref + .watch( + pAmountFormatter(coin), + ) + .format(fee) + : "Pending" + : ref + .watch(pAmountFormatter(coin)) + .format(fee); return Row( mainAxisAlignment: @@ -1413,41 +1456,38 @@ class _TransactionV2DetailsViewState children: [ Text( "Transaction fee", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (supportsRbf && !confirmedTxn) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (supportsRbf && !confirmedTxn) CustomTextButton( text: "Boost transaction", @@ -1458,19 +1498,21 @@ class _TransactionV2DetailsViewState if (!isDesktop) SelectableText( feeString, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: feeString), @@ -1481,9 +1523,7 @@ class _TransactionV2DetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), Builder( builder: (context) { final String height; @@ -1494,10 +1534,11 @@ class _TransactionV2DetailsViewState if (widget.coin is Bitcoincash || widget.coin is Ecash) { - height = _transaction.height != null && - _transaction.height! > 0 - ? "${_transaction.height!}" - : "Pending"; + height = + _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; confirmations = confirms.toString(); } else if (widget.coin is Epiccash && _transaction.slateId == null) { @@ -1505,16 +1546,18 @@ class _TransactionV2DetailsViewState height = "Unknown"; } else { final confirmed = _transaction.isConfirmed( - currentHeight, - minConfirms, - coin.minCoinbaseConfirms); + currentHeight, + minConfirms, + coin.minCoinbaseConfirms, + ); if (widget.coin is! Epiccash && confirmed) { height = "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; } else { - height = confirms > 0 - ? "${_transaction.height}" - : "Pending"; + height = + confirms > 0 + ? "${_transaction.height}" + : "Pending"; } confirmations = confirms.toString(); @@ -1523,9 +1566,10 @@ class _TransactionV2DetailsViewState return Column( children: [ RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1538,56 +1582,58 @@ class _TransactionV2DetailsViewState children: [ Text( "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: height), @@ -1596,13 +1642,12 @@ class _TransactionV2DetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -1615,56 +1660,58 @@ class _TransactionV2DetailsViewState children: [ Text( "Confirmations", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), if (isDesktop) - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), if (isDesktop) SelectableText( confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), if (!isDesktop) SelectableText( confirmations, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (isDesktop) IconCopyButton(data: height), @@ -1675,18 +1722,65 @@ class _TransactionV2DetailsViewState ); }, ), - - if (kDebugMode) + if (coin is Ethereum && + _transaction.type != TransactionType.incoming) isDesktop ? const _Divider() - : const SizedBox( - height: 12, + : const SizedBox(height: 12), + if (coin is Ethereum && + _transaction.type != TransactionType.incoming) + RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nonce", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), + SelectableText( + _transaction.nonce.toString(), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + ], + ), + ), + if (kDebugMode) + isDesktop + ? const _Divider() + : const SizedBox(height: 12), if (kDebugMode) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1694,25 +1788,32 @@ class _TransactionV2DetailsViewState children: [ Text( "Tx sub type", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), SelectableText( _transaction.subType.toString(), - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), ], ), @@ -1720,9 +1821,7 @@ class _TransactionV2DetailsViewState if (hasTxKeyProbably) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (hasTxKeyProbably) TxKeyWidget( walletId: walletId, @@ -1730,13 +1829,12 @@ class _TransactionV2DetailsViewState ), isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1749,61 +1847,61 @@ class _TransactionV2DetailsViewState children: [ ConditionalParent( condition: !isDesktop, - builder: (child) => Row( - children: [ - Expanded(child: child), - SimpleCopyButton( - data: _transaction.txid, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: _transaction.txid, + ), + ], ), - ], - ), child: Text( "Transaction ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), // Flexible( // child: FittedBox( // fit: BoxFit.scaleDown, // child: SelectableText( _transaction.txid, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), if (coin is! Epiccash) - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), if (coin is! Epiccash) CustomTextButton( text: "Open in block explorer", onTap: () async { final uri = getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); + coin: coin, + txid: _transaction.txid, + ); if (ref .read( @@ -1813,8 +1911,8 @@ class _TransactionV2DetailsViewState false) { final shouldContinue = await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); + "${uri.scheme}://${uri.host}", + ); if (!shouldContinue) { return; @@ -1829,21 +1927,22 @@ class _TransactionV2DetailsViewState try { await launchUrl( uri, - mode: LaunchMode - .externalApplication, + mode: + LaunchMode + .externalApplication, ); } catch (_) { - if (mounted) { + if (context.mounted) { unawaited( showDialog( context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), + builder: + (_) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), ); } @@ -1864,14 +1963,9 @@ class _TransactionV2DetailsViewState ], ), ), + if (isDesktop) const SizedBox(width: 12), if (isDesktop) - const SizedBox( - width: 12, - ), - if (isDesktop) - IconCopyButton( - data: _transaction.txid, - ), + IconCopyButton(data: _transaction.txid), ], ), ), @@ -1955,14 +2049,13 @@ class _TransactionV2DetailsViewState if (coin is Epiccash) isDesktop ? const _Divider() - : const SizedBox( - height: 12, - ), + : const SizedBox(height: 12), if (coin is Epiccash) RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: @@ -1974,14 +2067,14 @@ class _TransactionV2DetailsViewState children: [ Text( "Slate ID", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), // Flexible( // child: FittedBox( @@ -1989,27 +2082,27 @@ class _TransactionV2DetailsViewState // child: SelectableText( _transaction.slateId ?? "Unknown", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), ), // ), // ), ], ), - if (isDesktop) - const SizedBox( - width: 12, - ), + if (isDesktop) const SizedBox(width: 12), if (isDesktop) IconCopyButton( data: _transaction.slateId ?? "Unknown", @@ -2017,10 +2110,7 @@ class _TransactionV2DetailsViewState ], ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), // if (whatIsIt( // _transaction, // currentHeight, @@ -2042,101 +2132,106 @@ class _TransactionV2DetailsViewState ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: (coin is Epiccash && - _transaction.getConfirmations(currentHeight) < 1 && - _transaction.isCancelled == false) - ? ConditionalParent( - condition: isDesktop, - builder: (child) => Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - child: child, - ), - child: SizedBox( - width: MediaQuery.of(context).size.width - 32, - child: TextButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).extension()!.textError, + floatingActionButton: + (coin is Epiccash && + _transaction.getConfirmations(currentHeight) < 1 && + _transaction.isCancelled == false) + ? ConditionalParent( + condition: isDesktop, + builder: + (child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, ), - ), - onPressed: () async { - final wallet = ref.read(pWallets).getWallet(walletId); + child: SizedBox( + width: MediaQuery.of(context).size.width - 32, + child: TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).extension()!.textError, + ), + ), + onPressed: () async { + final wallet = ref.read(pWallets).getWallet(walletId); + + if (wallet is EpiccashWallet) { + final String? id = _transaction.slateId; + if (id == null) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Could not find Epic transaction ID", + context: context, + ), + ); + return; + } - if (wallet is EpiccashWallet) { - final String? id = _transaction.slateId; - if (id == null) { unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Could not find Epic transaction ID", + showDialog( + barrierDismissible: false, context: context, + builder: + (_) => + const CancellingTransactionProgressDialog(), ), ); - return; - } - - unawaited( - showDialog( - barrierDismissible: false, - context: context, - builder: (_) => - const CancellingTransactionProgressDialog(), - ), - ); - final result = - await wallet.cancelPendingTransactionAndPost(id); - if (mounted) { - // pop progress dialog - Navigator.of(context).pop(); - - if (result.isEmpty) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Transaction cancelled", - onOkPressed: (_) { - wallet.refresh(); - Navigator.of(context).popUntil( - ModalRoute.withName( - WalletView.routeName, + final result = await wallet + .cancelPendingTransactionAndPost(id); + if (context.mounted) { + // pop progress dialog + Navigator.of(context).pop(); + + if (result.isEmpty) { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Transaction cancelled", + onOkPressed: (_) { + wallet.refresh(); + Navigator.of(context).popUntil( + ModalRoute.withName( + WalletView.routeName, + ), + ); + }, ), - ); - }, - ), - ); - } else { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Failed to cancel transaction", - message: result, - ), - ); + ); + } else { + await showDialog( + context: context, + builder: + (_) => StackOkDialog( + title: "Failed to cancel transaction", + message: result, + ), + ); + } } + } else { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "ERROR: Wallet type is not Epic Cash", + context: context, + ), + ); + return; } - } else { - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "ERROR: Wallet type is not Epic Cash", - context: context, - ), - ); - return; - } - }, - child: Text( - "Cancel Transaction", - style: STextStyles.button(context), + }, + child: Text( + "Cancel Transaction", + style: STextStyles.button(context), + ), ), ), - ), - ) - : null, + ) + : null, ), ); } @@ -2161,38 +2256,44 @@ class OutputCard extends ConsumerWidget { children: [ Text( "Address", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), SelectableText( address, - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textDark, - ) - : STextStyles.itemSubtitle12(context), - ), - const SizedBox( - height: 10, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), ), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - : STextStyles.itemSubtitle(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + : STextStyles.itemSubtitle(context), ), SelectableText( ref.watch(pAmountFormatter(coin)).format(amount), - style: Util.isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textDark, - ) - : STextStyles.itemSubtitle12(context), + style: + Util.isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark, + ) + : STextStyles.itemSubtitle12(context), ), ], ), @@ -2214,10 +2315,7 @@ class _Divider extends StatelessWidget { } class IconCopyButton extends StatelessWidget { - const IconCopyButton({ - super.key, - required this.data, - }); + const IconCopyButton({super.key, required this.data}); final String data; @@ -2231,9 +2329,7 @@ class IconCopyButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); if (context.mounted) { @@ -2260,10 +2356,7 @@ class IconCopyButton extends StatelessWidget { } class IconPencilButton extends StatelessWidget { - const IconPencilButton({ - super.key, - this.onPressed, - }); + const IconPencilButton({super.key, this.onPressed}); final VoidCallback? onPressed; @@ -2277,9 +2370,7 @@ class IconPencilButton extends StatelessWidget { Theme.of(context).extension()!.buttonBackSecondary, elevation: 0, hoverElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), onPressed: () => onPressed?.call(), child: Padding( padding: const EdgeInsets.all(5), 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/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index d9f730804..1133731dc 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -10,6 +10,7 @@ import 'dart:io'; +import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -67,53 +68,63 @@ class _FavoriteCardState extends ConsumerState { prefsChangeNotifierProvider.select((value) => value.externalCalls), ); + Decimal? price; + if (externalCalls) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } return ConditionalParent( condition: Util.isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) { - setState(() { - _hovering = true; - }); - }, - onExit: (_) { - setState(() { - _hovering = false; - }); - }, - child: AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: _hovering ? 1.05 : 1, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: _hovering - ? BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - boxShadow: [ - Theme.of(context) - .extension()! - .standardBoxShadow, - Theme.of(context) - .extension()! - .standardBoxShadow, - Theme.of(context) - .extension()! - .standardBoxShadow, - ], - ) - : BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - child: child, + builder: + (child) => MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) { + setState(() { + _hovering = true; + }); + }, + onExit: (_) { + setState(() { + _hovering = false; + }); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: _hovering ? 1.05 : 1, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: + _hovering + ? BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + boxShadow: [ + Theme.of( + context, + ).extension()!.standardBoxShadow, + Theme.of( + context, + ).extension()!.standardBoxShadow, + Theme.of( + context, + ).extension()!.standardBoxShadow, + ], + ) + : BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: child, + ), + ), ), - ), - ), child: GestureDetector( onTap: () async { final wallet = ref.read(pWallets).getWallet(walletId); @@ -133,8 +144,9 @@ class _FavoriteCardState extends ConsumerState { final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -147,15 +159,13 @@ class _FavoriteCardState extends ConsumerState { if (mounted) { if (Util.isDesktop) { - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: walletId, - ); + await Navigator.of( + context, + ).pushNamed(DesktopWalletView.routeName, arguments: walletId); } else { - await Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: walletId, - ); + await Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: walletId); } } }, @@ -183,17 +193,16 @@ class _FavoriteCardState extends ConsumerState { child: Text( ref.watch(pWalletName(walletId)), style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of( + context, + ).extension()!.textFavoriteCard, ), overflow: TextOverflow.fade, ), ), SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 24, height: 24, ), @@ -202,36 +211,24 @@ class _FavoriteCardState extends ConsumerState { ), Builder( builder: (context) { - final balance = ref.watch( - pWalletBalance(walletId), - ); + final balance = ref.watch(pWalletBalance(walletId)); Amount total = balance.total; if (coin is Firo) { - total += ref - .watch( - pWalletBalanceSecondary(walletId), - ) - .total; - total += ref - .watch( - pWalletBalanceTertiary(walletId), - ) - .total; + total += + ref.watch(pWalletBalanceSecondary(walletId)).total; + total += + ref.watch(pWalletBalanceTertiary(walletId)).total; } Amount fiatTotal = Amount.zero; - if (externalCalls && total > Amount.zero) { - fiatTotal = (total.decimal * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1) - .toAmount(fractionDigits: 2); + if (externalCalls && + total > Amount.zero && + price != null) { + fiatTotal = (total.decimal * price).toAmount( + fractionDigits: 2, + ); } return Column( @@ -243,35 +240,26 @@ class _FavoriteCardState extends ConsumerState { ref.watch(pAmountFormatter(coin)).format(total), style: STextStyles.titleBold12(context).copyWith( fontSize: 16, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of(context) + .extension()! + .textFavoriteCard, ), ), ), - if (externalCalls) - const SizedBox( - height: 4, - ), - if (externalCalls) + if (externalCalls && price != null) + const SizedBox(height: 4), + if (externalCalls && price != null) Text( - "${fiatTotal.fiatString( - locale: ref.watch( - localeServiceChangeNotifierProvider.select( - (value) => value.locale, - ), - ), - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), - )}", - style: - STextStyles.itemSubtitle12(context).copyWith( + "${fiatTotal.fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.itemSubtitle12( + context, + ).copyWith( fontSize: 10, - color: Theme.of(context) - .extension()! - .textFavoriteCard, + color: + Theme.of(context) + .extension()! + .textFavoriteCard, ), ), ], @@ -300,11 +288,6 @@ class CardOverlayStack extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - background, - child, - ], - ); + return Stack(children: [background, child]); } } diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 938baac71..64d9ecbc9 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -46,8 +46,9 @@ class WalletListItem extends ConsumerWidget { // debugPrint("BUILD: $runtimeType"); final walletCountString = walletCount == 1 ? "$walletCount wallet" : "$walletCount wallets"; - final currency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final currency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); return RoundedWhiteContainer( padding: const EdgeInsets.all(0), @@ -57,8 +58,9 @@ class WalletListItem extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), onPressed: () async { // Check if Tor is enabled... @@ -66,11 +68,10 @@ class WalletListItem extends ConsumerWidget { // ... and if the coin supports Tor. if (!coin.torSupport) { // If not, show a Tor warning dialog. - final shouldContinue = await showDialog( + final shouldContinue = + await showDialog( context: context, - builder: (_) => TorWarningDialog( - coin: coin, - ), + builder: (_) => TorWarningDialog(coin: coin), ) ?? false; if (!shouldContinue) { @@ -100,8 +101,9 @@ class WalletListItem extends ConsumerWidget { final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -113,63 +115,67 @@ class WalletListItem extends ConsumerWidget { ); if (context.mounted) { unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: wallet.walletId, - ), + Navigator.of( + context, + ).pushNamed(WalletView.routeName, arguments: wallet.walletId), ); } } else { unawaited( - Navigator.of(context).pushNamed( - WalletsOverview.routeName, - arguments: coin, - ), + Navigator.of( + context, + ).pushNamed(WalletsOverview.routeName, arguments: coin), ); } }, child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 28, height: 28, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Expanded( child: Consumer( builder: (_, ref, __) { - final tuple = ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin)), - ); - final calls = - ref.watch(prefsChangeNotifierProvider).externalCalls; + Color percentChangedColor = + Theme.of(context).extension()!.textDark; + String? priceString; + double? percentChange; + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) { + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ); - final priceString = - tuple.item1.toAmount(fractionDigits: 2).fiatString( + if (price != null) { + priceString = price.value + .toAmount(fractionDigits: 2) + .fiatString( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ); + percentChange = price.change24h; - final double percentChange = tuple.item2; - - var percentChangedColor = - Theme.of(context).extension()!.textDark; - if (percentChange > 0) { - percentChangedColor = Theme.of(context) - .extension()! - .accentColorGreen; - } else if (percentChange < 0) { - percentChangedColor = Theme.of(context) - .extension()! - .accentColorRed; + if (percentChange > 0) { + percentChangedColor = + Theme.of( + context, + ).extension()!.accentColorGreen; + } else if (percentChange < 0) { + percentChangedColor = + Theme.of( + context, + ).extension()!.accentColorRed; + } + } } return Column( @@ -182,16 +188,14 @@ class WalletListItem extends ConsumerWidget { style: STextStyles.titleBold12(context), ), const Spacer(), - if (calls) + if (priceString != null) Text( "$priceString $currency/${coin.ticker}", style: STextStyles.itemSubtitle(context), ), ], ), - const SizedBox( - height: 1, - ), + const SizedBox(height: 1), Row( children: [ Text( @@ -199,12 +203,12 @@ class WalletListItem extends ConsumerWidget { style: STextStyles.itemSubtitle(context), ), const Spacer(), - if (calls) + if (percentChange != null) Text( "${percentChange.toStringAsFixed(2)}%", - style: STextStyles.itemSubtitle(context).copyWith( - color: percentChangedColor, - ), + style: STextStyles.itemSubtitle( + context, + ).copyWith(color: percentChangedColor), ), ], ), diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index c487ff991..d2a053d74 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -19,7 +19,7 @@ import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; import '../../db/isar/main_db.dart'; -import '../../models/exchange/change_now/exchange_transaction_status.dart'; +import '../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/isar/stack_theme.dart'; @@ -104,41 +104,32 @@ class _DesktopAllTradesViewState extends ConsumerState { background: Theme.of(context).extension()!.popupBG, leading: Row( children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 12, - ), - Text( - "Trades", - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 12), + Text("Trades", style: STextStyles.desktopH3(context)), ], ), ), body: Padding( - padding: const EdgeInsets.only( - left: 20, - top: 20, - right: 20, - ), + padding: const EdgeInsets.only(left: 20, top: 20, right: 20), child: Column( children: [ Row( @@ -159,11 +150,13 @@ class _DesktopAllTradesViewState extends ConsumerState { _searchString = value; }); }, - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -183,35 +176,34 @@ class _DesktopAllTradesViewState extends ConsumerState { height: 20, ), ), - suffixIcon: _searchController.text.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(right: 0), - child: UnconstrainedBox( - child: Row( - children: [ - TextFieldIconButton( - child: const XIcon(), - onTap: () async { - setState(() { - _searchController.text = ""; - _searchString = ""; - }); - }, - ), - ], + suffixIcon: + _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), ), - ), - ) - : null, + ) + : null, ), ), ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Expanded( child: Consumer( builder: (_, ref, __) { @@ -237,38 +229,36 @@ class _DesktopAllTradesViewState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (index != 0) - const SizedBox( - height: 12, - ), + if (index != 0) const SizedBox(height: 12), Text( month.item1, style: STextStyles.smallMed12(context), ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), RoundedWhiteContainer( padding: const EdgeInsets.all(0), child: ListView.separated( shrinkWrap: true, primary: false, - separatorBuilder: (context, _) => Container( - height: 1, - color: Theme.of(context) - .extension()! - .background, - ), + separatorBuilder: + (context, _) => Container( + height: 1, + color: + Theme.of(context) + .extension()! + .background, + ), itemCount: month.item2.length, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.all(4), - child: DesktopTradeRowCard( - key: Key( - "transactionCard_key_${month.item2[index].tradeId}", + itemBuilder: + (context, index) => Padding( + padding: const EdgeInsets.all(4), + child: DesktopTradeRowCard( + key: Key( + "transactionCard_key_${month.item2[index].tradeId}", + ), + tradeId: month.item2[index].tradeId, + ), ), - tradeId: month.item2[index].tradeId, - ), - ), ), ), ], @@ -287,10 +277,7 @@ class _DesktopAllTradesViewState extends ConsumerState { } class DesktopTradeRowCard extends ConsumerStatefulWidget { - const DesktopTradeRowCard({ - super.key, - required this.tradeId, - }); + const DesktopTradeRowCard({super.key, required this.tradeId}); final String tradeId; @@ -347,8 +334,9 @@ class _DesktopTradeRowCardState extends ConsumerState { @override Widget build(BuildContext context) { - final String? txid = - ref.read(tradeSentFromStackLookupProvider).getTxidForTradeId(tradeId); + final String? txid = ref + .read(tradeSentFromStackLookupProvider) + .getTxidForTradeId(tradeId); final List? walletIds = ref .read(tradeSentFromStackLookupProvider) .getWalletIdsForTradeId(tradeId); @@ -360,8 +348,9 @@ class _DesktopTradeRowCardState extends ConsumerState { color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: RawMaterialButton( shape: RoundedRectangleBorder( @@ -374,25 +363,27 @@ class _DesktopTradeRowCardState extends ConsumerState { //todo: check if print needed // debugPrint("name: ${manager.walletName}"); - final tx = await MainDB.instance - .getTransactions(walletIds.first) - .filter() - .txidEqualTo(txid) - .findFirst(); + final tx = + await MainDB.instance + .getTransactions(walletIds.first) + .filter() + .txidEqualTo(txid) + .findFirst(); if (mounted) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: ref.read(pWalletName(walletIds.first)), - walletId: walletIds.first, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: ref.read(pWalletName(walletIds.first)), + walletId: walletIds.first, + ), + ), ); } @@ -400,62 +391,67 @@ class _DesktopTradeRowCardState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), + builder: + (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3( + context, + ), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: SingleChildScrollView( - primary: false, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: tx, - walletName: ref - .read(pWalletName(walletIds.first)), - walletId: walletIds.first, ), - ), + Flexible( + child: SingleChildScrollView( + primary: false, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: tx, + walletName: ref.read( + pWalletName(walletIds.first), + ), + walletId: walletIds.first, + ), + ), + ), + ], ), - ], + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ), ); } @@ -463,70 +459,69 @@ class _DesktopTradeRowCardState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Navigator( - initialRoute: TradeDetailsView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - DesktopDialog( - maxHeight: null, - maxWidth: 580, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - bottom: 16, - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - "Trade details", - style: STextStyles.desktopH3(context), + builder: + (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: SingleChildScrollView( - primary: false, - child: TradeDetailsView( - tradeId: tradeId, - transactionIfSentFromStack: null, - walletName: null, - walletId: walletIds?.first, ), - ), + Flexible( + child: SingleChildScrollView( + primary: false, + child: TradeDetailsView( + tradeId: tradeId, + transactionIfSentFromStack: null, + walletName: null, + walletId: walletIds?.first, + ), + ), + ), + ], ), - ], + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), ), - ), - const RouteSettings( - name: TradeDetailsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), child: Row( children: [ Container( @@ -540,9 +535,7 @@ class _DesktopTradeRowCardState extends ConsumerState { File( _fetchIconAssetForStatus( trade.status, - ref.watch( - themeAssetsProvider, - ), + ref.watch(themeAssetsProvider), ), ), width: 32, @@ -550,15 +543,14 @@ class _DesktopTradeRowCardState extends ConsumerState { ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( flex: 3, child: Text( "${trade.payInCurrency.toUpperCase()} → ${trade.payOutCurrency.toUpperCase()}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), @@ -576,8 +568,9 @@ class _DesktopTradeRowCardState extends ConsumerState { flex: 6, child: Text( "-${Decimal.tryParse(trade.payInAmount)?.toStringAsFixed(8) ?? "..."} ${trade.payInCurrency.toUpperCase()}", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( color: Theme.of(context).extension()!.textDark, ), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index c6769e68a..3fe2b20d7 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -47,14 +47,11 @@ final ssss = StateProvider((_) => null); final desktopExchangeModelProvider = ChangeNotifierProvider( - (ref) => ref.watch(ssss.state).state, -); + (ref) => ref.watch(ssss.state).state, + ); class StepScaffold extends ConsumerStatefulWidget { - const StepScaffold({ - super.key, - required this.initialStep, - }); + const StepScaffold({super.key, required this.initialStep}); final int initialStep; @@ -79,19 +76,19 @@ class _StepScaffoldState extends ConsumerState { showDialog( context: context, barrierDismissible: false, - builder: (_) => WillPopScope( - onWillPop: () async => false, - child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.6), - child: const CustomLoadingOverlay( - message: "Creating a trade", - eventBus: null, + builder: + (_) => WillPopScope( + onWillPop: () async => false, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.6), + child: const CustomLoadingOverlay( + message: "Creating a trade", + eventBus: null, + ), + ), ), - ), - ), ), ); @@ -99,12 +96,16 @@ class _StepScaffoldState extends ConsumerState { .read(efExchangeProvider) .createTrade( from: ref.read(desktopExchangeModelProvider)!.sendTicker, + fromNetwork: ref.read(desktopExchangeModelProvider)!.sendNetwork, to: ref.read(desktopExchangeModelProvider)!.receiveTicker, - fixedRate: ref.read(desktopExchangeModelProvider)!.rateType != + toNetwork: ref.read(desktopExchangeModelProvider)!.receiveNetwork, + fixedRate: + ref.read(desktopExchangeModelProvider)!.rateType != ExchangeRateType.estimated, - amount: ref.read(desktopExchangeModelProvider)!.reversed - ? ref.read(desktopExchangeModelProvider)!.receiveAmount - : ref.read(desktopExchangeModelProvider)!.sendAmount, + amount: + ref.read(desktopExchangeModelProvider)!.reversed + ? ref.read(desktopExchangeModelProvider)!.receiveAmount + : ref.read(desktopExchangeModelProvider)!.sendAmount, addressTo: ref.read(desktopExchangeModelProvider)!.recipientAddress!, extraId: null, addressRefund: ref.read(desktopExchangeModelProvider)!.refundAddress!, @@ -131,10 +132,11 @@ class _StepScaffoldState extends ConsumerState { showDialog( context: context, barrierDismissible: true, - builder: (_) => SimpleDesktopDialog( - title: "Failed to create trade", - message: message ?? "", - ), + builder: + (_) => SimpleDesktopDialog( + title: "Failed to create trade", + message: message ?? "", + ), ), ); } @@ -142,10 +144,9 @@ class _StepScaffoldState extends ConsumerState { } // save trade to hive - await ref.read(tradesServiceProvider).add( - trade: response.value!, - shouldNotifyListeners: true, - ); + await ref + .read(tradesServiceProvider) + .add(trade: response.value!, shouldNotifyListeners: true); String status = response.value!.status; @@ -206,35 +207,35 @@ class _StepScaffoldState extends ConsumerState { void sendFromStack() { final trade = ref.read(desktopExchangeModelProvider)!.trade!; final address = trade.payInAddress; - final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ?? + final coin = + AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ?? AppConfig.getCryptoCurrencyByPrettyName(trade.payInCurrency); - final amount = Decimal.parse(trade.payInAmount).toAmount( - fractionDigits: coin.fractionDigits, - ); + final amount = Decimal.parse( + trade.payInAmount, + ).toAmount(fractionDigits: coin.fractionDigits); showDialog( context: context, - builder: (context) => Navigator( - initialRoute: SendFromView.routeName, - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) { - return [ - FadePageRoute( - SendFromView( - coin: coin, - trade: trade, - amount: amount, - address: address, - shouldPopRoot: true, - fromDesktopStep4: true, - ), - const RouteSettings( - name: SendFromView.routeName, - ), - ), - ]; - }, - ), + builder: + (context) => Navigator( + initialRoute: SendFromView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + SendFromView( + coin: coin, + trade: trade, + amount: amount, + address: address, + shouldPopRoot: true, + fromDesktopStep4: true, + ), + const RouteSettings(name: SendFromView.routeName), + ), + ]; + }, + ), ); } @@ -258,13 +259,11 @@ class _StepScaffoldState extends ConsumerState { children: [ currentStep != 4 ? AppBarBackButton( - isCompact: true, - iconSize: 23, - onPressed: onBack, - ) - : const SizedBox( - width: 32, - ), + isCompact: true, + iconSize: 23, + onPressed: onBack, + ) + : const SizedBox(width: 32), Text( "Exchange ${model?.sendTicker.toUpperCase()} to ${model?.receiveTicker.toUpperCase()}", style: STextStyles.desktopH3(context), @@ -279,31 +278,19 @@ class _StepScaffoldState extends ConsumerState { ), ], ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: DesktopExchangeStepsIndicator( - currentStep: currentStep, - ), - ), - const SizedBox( - height: 32, + padding: const EdgeInsets.symmetric(horizontal: 32), + child: DesktopExchangeStepsIndicator(currentStep: currentStep), ), + const SizedBox(height: 32), Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(horizontal: 32), child: FadeStack( index: currentStep - 1, children: [ const DesktopStep1(), - DesktopStep2( - enableNextChanged: updateEnableNext, - ), + DesktopStep2(enableNextChanged: updateEnableNext), const DesktopStep3(), const DesktopStep4(), ], @@ -321,9 +308,10 @@ class _StepScaffoldState extends ConsumerState { Expanded( child: AnimatedCrossFade( duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 4 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + currentStep == 4 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: SecondaryButton( label: "Back", buttonHeight: ButtonHeight.l, @@ -336,20 +324,20 @@ class _StepScaffoldState extends ConsumerState { ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: AnimatedCrossFade( duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 4 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + currentStep == 4 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: AnimatedCrossFade( duration: const Duration(milliseconds: 250), - crossFadeState: currentStep == 3 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + currentStep == 3 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: PrimaryButton( label: "Next", enabled: currentStep != 2 ? true : enableNext, @@ -394,9 +382,7 @@ class _StepScaffoldState extends ConsumerState { "Send ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendAmount.toStringAsFixed(8)))} ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker))} to this address", style: STextStyles.desktopH3(context), ), - const SizedBox( - height: 48, - ), + const SizedBox(height: 48), Center( child: QR( // TODO: grab coin uri scheme from somewhere @@ -409,9 +395,7 @@ class _StepScaffoldState extends ConsumerState { size: 290, ), ), - const SizedBox( - height: 48, - ), + const SizedBox(height: 48), SecondaryButton( label: "Cancel", width: 310, diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index fd7c72a23..c7ba641ca 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -59,23 +59,23 @@ class _DesktopStep2State extends ConsumerState { void selectRecipientAddressFromStack() async { try { - final coin = AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.receiveTicker, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.receiveTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack( - coin: coin, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack(coin: coin), + ), ), - ), - ), ); if (info is Tuple2) { @@ -83,44 +83,40 @@ class _DesktopStep2State extends ConsumerState { ref.read(desktopExchangeModelProvider)!.recipientAddress = info.item2; } } catch (e, s) { - Logging.instance.i("$e\n$s", error: e, stackTrace: s,); + Logging.instance.i("$e\n$s", error: e, stackTrace: s); } - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } void selectRefundAddressFromStack() async { try { - final coin = AppConfig.getCryptoCurrencyForTicker( - ref.read(desktopExchangeModelProvider)!.sendTicker, - )!; + final coin = + AppConfig.getCryptoCurrencyForTicker( + ref.read(desktopExchangeModelProvider)!.sendTicker, + )!; final info = await showDialog?>( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Padding( - padding: const EdgeInsets.all(32), - child: DesktopChooseFromStack( - coin: coin, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + child: Padding( + padding: const EdgeInsets.all(32), + child: DesktopChooseFromStack(coin: coin), + ), ), - ), - ), ); if (info is Tuple2) { _refundController.text = info.item1; ref.read(desktopExchangeModelProvider)!.refundAddress = info.item2; } } catch (e, s) { - Logging.instance.i("$e\n$s", error: e, stackTrace: s,); + Logging.instance.i("$e\n$s", error: e, stackTrace: s); } - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } void selectRecipientFromAddressBook() async { @@ -131,43 +127,36 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + 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(), + ], ), - const DesktopDialogCloseButton(), + Expanded(child: AddressBookAddressChooser(coin: coin)), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { _toController.text = entry.address; ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } } @@ -179,43 +168,36 @@ class _DesktopStep2State extends ConsumerState { final entry = await showDialog( context: context, barrierColor: Colors.transparent, - builder: (context) => DesktopDialog( - maxWidth: 720, - maxHeight: 670, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 720, + maxHeight: 670, + 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(), + ], ), - const DesktopDialogCloseButton(), + Expanded(child: AddressBookAddressChooser(coin: coin)), ], ), - Expanded( - child: AddressBookAddressChooser( - coin: coin, - ), - ), - ], - ), - ), + ), ); if (entry != null) { _refundController.text = entry.address; ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); } } @@ -242,33 +224,41 @@ class _DesktopStep2State extends ConsumerState { doesRefundAddress = ref.read(efExchangeProvider).supportsRefundAddress; if (!doesRefundAddress) { - // hack: set to empty to not throw null unwrap error later - ref.read(desktopExchangeModelProvider)!.refundAddress = ""; + WidgetsBinding.instance.addPostFrameCallback((_) { + // hack: set to empty to not throw null unwrap error later + ref.read(desktopExchangeModelProvider)!.refundAddress = ""; + }); } final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state; if (tuple != null) { if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() == tuple.item2.ticker.toLowerCase()) { - _toController.text = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; - - ref.read(desktopExchangeModelProvider)!.recipientAddress = - _toController.text; + _toController.text = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(desktopExchangeModelProvider)!.recipientAddress = + _toController.text; + }); } else { if (doesRefundAddress && ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() == tuple.item2.ticker.toUpperCase()) { - _refundController.text = ref - .read(pWallets) - .getWallet(tuple.item1) - .info - .cachedReceivingAddress; - ref.read(desktopExchangeModelProvider)!.refundAddress = - _refundController.text; + _refundController.text = + ref + .read(pWallets) + .getWallet(tuple.item1) + .info + .cachedReceivingAddress; + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(desktopExchangeModelProvider)!.refundAddress = + _refundController.text; + }); } } } @@ -297,32 +287,30 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.desktopTextMedium(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Text( "Enter your recipient and refund addresses", style: STextStyles.desktopTextExtraExtraSmall(context), textAlign: TextAlign.center, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Recipient Wallet", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.receiveTicker), + desktopExchangeModelProvider.select( + (value) => value!.receiveTicker, + ), ), )) CustomTextButton( @@ -331,9 +319,7 @@ class _DesktopStep2State extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -359,9 +345,7 @@ class _DesktopStep2State extends ConsumerState { onChanged: (value) { ref.read(desktopExchangeModelProvider)!.recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); }, decoration: standardInputDecoration( "Enter the ${ref.watch(desktopExchangeModelProvider.select((value) => value!.receiveTicker.toUpperCase()))} payout address", @@ -376,57 +360,56 @@ class _DesktopStep2State extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _toController.text.isEmpty - ? const EdgeInsets.only(right: 8) - : const EdgeInsets.only(right: 0), + padding: + _toController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _toController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _toController.text = ""; + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _toController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .recipientAddress = _toController.text; + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + _toController.text = content; ref .read(desktopExchangeModelProvider)! .recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - _toController.text = content; - ref - .read(desktopExchangeModelProvider)! - .recipientAddress = _toController.text; - widget.enableNextChanged.call( - _next(), - ); - } - }, - child: _toController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + widget.enableNextChanged.call(_next()); + } + }, + child: + _toController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_toController.text.isEmpty && AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.receiveTicker), + desktopExchangeModelProvider.select( + (value) => value!.receiveTicker, + ), ), )) TextFieldIconButton( @@ -441,9 +424,7 @@ class _DesktopStep2State extends ConsumerState { ), ), ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, child: Text( @@ -451,10 +432,7 @@ class _DesktopStep2State extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall(context), ), ), - if (doesRefundAddress) - const SizedBox( - height: 24, - ), + if (doesRefundAddress) const SizedBox(height: 24), if (doesRefundAddress) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -462,15 +440,17 @@ class _DesktopStep2State extends ConsumerState { Text( "Refund Wallet (required)", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), if (AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), + desktopExchangeModelProvider.select( + (value) => value!.sendTicker, + ), ), )) CustomTextButton( @@ -479,10 +459,7 @@ class _DesktopStep2State extends ConsumerState { ), ], ), - if (doesRefundAddress) - const SizedBox( - height: 10, - ), + if (doesRefundAddress) const SizedBox(height: 10), if (doesRefundAddress) ClipRRect( borderRadius: BorderRadius.circular( @@ -508,9 +485,7 @@ class _DesktopStep2State extends ConsumerState { onChanged: (value) { ref.read(desktopExchangeModelProvider)!.refundAddress = _refundController.text; - widget.enableNextChanged.call( - _next(), - ); + widget.enableNextChanged.call(_next()); }, decoration: standardInputDecoration( "Enter ${ref.watch(desktopExchangeModelProvider.select((value) => value!.sendTicker.toUpperCase()))} refund address", @@ -525,60 +500,59 @@ class _DesktopStep2State extends ConsumerState { right: 5, ), suffixIcon: Padding( - padding: _refundController.text.isEmpty - ? const EdgeInsets.only(right: 16) - : const EdgeInsets.only(right: 0), + padding: + _refundController.text.isEmpty + ? const EdgeInsets.only(right: 16) + : const EdgeInsets.only(right: 0), child: UnconstrainedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _refundController.text.isNotEmpty ? TextFieldIconButton( - key: const Key( - "sendViewClearAddressFieldButtonKey", - ), - onTap: () { - _refundController.text = ""; + key: const Key( + "sendViewClearAddressFieldButtonKey", + ), + onTap: () { + _refundController.text = ""; + ref + .read(desktopExchangeModelProvider)! + .refundAddress = _refundController.text; + + widget.enableNextChanged.call(_next()); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendViewPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + final content = data.text!.trim(); + + _refundController.text = content; ref .read(desktopExchangeModelProvider)! .refundAddress = _refundController.text; - widget.enableNextChanged.call( - _next(), - ); - }, - child: const XIcon(), - ) - : TextFieldIconButton( - key: const Key( - "sendViewPasteAddressFieldButtonKey", - ), - onTap: () async { - final ClipboardData? data = await clipboard - .getData(Clipboard.kTextPlain); - if (data?.text != null && - data!.text!.isNotEmpty) { - final content = data.text!.trim(); - - _refundController.text = content; - ref - .read(desktopExchangeModelProvider)! - .refundAddress = _refundController.text; - - widget.enableNextChanged.call( - _next(), - ); - } - }, - child: _refundController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), - ), + widget.enableNextChanged.call(_next()); + } + }, + child: + _refundController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (_refundController.text.isEmpty && AppConfig.isStackCoin( ref.watch( - desktopExchangeModelProvider - .select((value) => value!.sendTicker), + desktopExchangeModelProvider.select( + (value) => value!.sendTicker, + ), ), )) TextFieldIconButton( @@ -593,10 +567,7 @@ class _DesktopStep2State extends ConsumerState { ), ), ), - if (doesRefundAddress) - const SizedBox( - height: 10, - ), + if (doesRefundAddress) const SizedBox(height: 10), if (doesRefundAddress) RoundedWhiteContainer( borderColor: Theme.of(context).extension()!.background, diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index 023b9fb79..476a3eb24 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -10,6 +10,7 @@ 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'; @@ -63,6 +64,15 @@ class _DesktopPaynymSendDialogState final coin = ref.watch(pWalletCoin(widget.walletId)); + Decimal? price; + if (ref.watch(prefsChangeNotifierProvider.select((s) => s.externalCalls))) { + price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin)?.value, + ), + ); + } + return DesktopDialog( maxHeight: double.infinity, maxWidth: 580, @@ -90,15 +100,11 @@ class _DesktopPaynymSendDialogState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), + File(ref.watch(coinIconProvider(coin))), width: 36, height: 36, ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -108,15 +114,14 @@ class _DesktopPaynymSendDialogState overflow: TextOverflow.ellipsis, maxLines: 1, ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Text( "Available balance", style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], @@ -128,7 +133,9 @@ class _DesktopPaynymSendDialogState crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( ref .watch(pWalletBalance(widget.walletId)) .spendable, @@ -136,28 +143,18 @@ class _DesktopPaynymSendDialogState style: STextStyles.titleBold12(context), textAlign: TextAlign.right, ), - const SizedBox( - height: 2, - ), - Text( - "${(ref.watch(pWalletBalance(widget.walletId)).spendable.decimal * ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin).item1, - ), - )).toAmount(fractionDigits: 2).fiatString( - locale: locale, - )} ${ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, + if (price != null) const SizedBox(height: 2), + if (price != null) + Text( + "${(ref.watch(pWalletBalance(widget.walletId)).spendable.decimal * price).toAmount(fractionDigits: 2).fiatString(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.baseXS(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), - )}", - style: STextStyles.baseXS(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + textAlign: TextAlign.right, ), - textAlign: TextAlign.right, - ), ], ), ), @@ -165,15 +162,9 @@ class _DesktopPaynymSendDialogState ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), + padding: const EdgeInsets.only(left: 32, right: 32, bottom: 32), child: DesktopSend( walletId: widget.walletId, accountLite: widget.accountLite, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index 066ae9073..45b5d710e 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -10,6 +10,7 @@ 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'; @@ -53,12 +54,11 @@ class _WalletTableState extends ConsumerState { return ConditionalParent( condition: index + 1 == walletsByCoin.length, - builder: (child) => Padding( - padding: const EdgeInsets.only( - bottom: 16, - ), - child: child, - ), + builder: + (child) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: child, + ), child: DesktopWalletSummaryRow( key: Key("DesktopWalletSummaryRow_key_${coin.identifier}"), coin: coin, @@ -66,9 +66,7 @@ class _WalletTableState extends ConsumerState { ), ); }, - separatorBuilder: (_, __) => const SizedBox( - height: 10, - ), + separatorBuilder: (_, __) => const SizedBox(height: 10), itemCount: walletsByCoin.length, ); } @@ -96,11 +94,10 @@ class _DesktopWalletSummaryRowState // ... and if the coin supports Tor. if (!widget.coin.torSupport) { // If not, show a Tor warning dialog. - final shouldContinue = await showDialog( + final shouldContinue = + await showDialog( context: context, - builder: (_) => TorWarningDialog( - coin: widget.coin, - ), + builder: (_) => TorWarningDialog(coin: widget.coin), ) ?? false; if (!shouldContinue) { @@ -121,8 +118,12 @@ class _DesktopWalletSummaryRowState await _checkTor(); if (mounted) { - final wallet = ref.read(pWallets).wallets.firstWhere( - (e) => e.cryptoCurrency.identifier == widget.coin.identifier); + final wallet = ref + .read(pWallets) + .wallets + .firstWhere( + (e) => e.cryptoCurrency.identifier == widget.coin.identifier, + ); final canContinue = await checkShowNodeTorSettingsMismatch( context: context, @@ -139,8 +140,9 @@ class _DesktopWalletSummaryRowState final Future loadFuture; if (wallet is ExternalWallet) { - loadFuture = - wallet.init().then((value) async => await (wallet).open()); + loadFuture = wallet.init().then( + (value) async => await (wallet).open(), + ); } else { loadFuture = wallet.init(); } @@ -152,10 +154,9 @@ class _DesktopWalletSummaryRowState ); if (mounted) { - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: wallet.walletId, - ); + await Navigator.of( + context, + ).pushNamed(DesktopWalletView.routeName, arguments: wallet.walletId); } } } finally { @@ -173,40 +174,41 @@ class _DesktopWalletSummaryRowState if (mounted) { await showDialog( context: context, - builder: (_) => DesktopDialog( - maxHeight: 600, - maxWidth: 700, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (_) => DesktopDialog( + maxHeight: 600, + maxWidth: 700, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", - style: STextStyles.desktopH3(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: WalletsOverview( + coin: widget.coin, + navigatorState: Navigator.of(context), + ), ), ), - const DesktopDialogCloseButton(), ], ), - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: WalletsOverview( - coin: widget.coin, - navigatorState: Navigator.of(context), - ), - ), - ), - ], - ), - ), + ), ); } } finally { @@ -216,6 +218,12 @@ class _DesktopWalletSummaryRowState @override Widget build(BuildContext context) { + final price = ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(widget.coin), + ), + ); + return Breathing( child: RoundedWhiteContainer( padding: const EdgeInsets.all(20), @@ -229,15 +237,11 @@ class _DesktopWalletSummaryRowState child: Row( children: [ SvgPicture.file( - File( - ref.watch(coinIconProvider(widget.coin)), - ), + File(ref.watch(coinIconProvider(widget.coin))), width: 28, height: 28, ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Text( widget.coin.prettyName, style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -260,12 +264,11 @@ class _DesktopWalletSummaryRowState ), ), ), - Expanded( - flex: 6, - child: TablePriceInfo( - coin: widget.coin, + if (price != null) + Expanded( + flex: 6, + child: TablePriceInfo(coin: widget.coin, price: price), ), - ), ], ), ), @@ -274,43 +277,30 @@ class _DesktopWalletSummaryRowState } class TablePriceInfo extends ConsumerWidget { - const TablePriceInfo({super.key, required this.coin}); + const TablePriceInfo({super.key, required this.coin, required this.price}); final CryptoCurrency coin; + final ({Decimal value, double change24h}) price; @override Widget build(BuildContext context, WidgetRef ref) { - final tuple = ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ); - final currency = ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.currency, - ), + prefsChangeNotifierProvider.select((value) => value.currency), ); final priceString = Amount.fromDecimal( - tuple.item1, + price.value, fractionDigits: 2, ).fiatString( - locale: ref - .watch( - localeServiceChangeNotifierProvider.notifier, - ) - .locale, + locale: ref.watch(localeServiceChangeNotifierProvider.notifier).locale, ); - final double percentChange = tuple.item2; - var percentChangedColor = Theme.of(context).extension()!.textDark; - if (percentChange > 0) { + if (price.change24h > 0) { percentChangedColor = Theme.of(context).extension()!.accentColorGreen; - } else if (percentChange < 0) { + } else if (price.change24h < 0) { percentChangedColor = Theme.of(context).extension()!.accentColorRed; } @@ -325,10 +315,10 @@ class TablePriceInfo extends ConsumerWidget { ), ), Text( - "${percentChange.toStringAsFixed(2)}%", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: percentChangedColor, - ), + "${price.change24h.toStringAsFixed(2)}%", + style: STextStyles.desktopTextExtraSmall( + context, + ).copyWith(color: percentChangedColor), ), ], ); 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_fee_dropdown.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart deleted file mode 100644 index 6326a6474..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ /dev/null @@ -1,447 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter/material.dart'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; - -import '../../../../models/models.dart'; -import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; -import '../../../../providers/global/wallets_provider.dart'; -import '../../../../providers/ui/fee_rate_type_state_provider.dart'; -import '../../../../providers/wallet/public_private_balance_state_provider.dart'; -import '../../../../themes/stack_colors.dart'; -import '../../../../utilities/amount/amount.dart'; -import '../../../../utilities/amount/amount_formatter.dart'; -import '../../../../utilities/assets.dart'; -import '../../../../utilities/constants.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; -import '../../../../utilities/text_styles.dart'; -import '../../../../wallets/crypto_currency/crypto_currency.dart'; -import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; -import '../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../wallets/wallet/impl/firo_wallet.dart'; -import '../../../../widgets/animated_text.dart'; - -final tokenFeeSessionCacheProvider = - ChangeNotifierProvider((ref) { - return FeeSheetSessionCache(); -}); - -class DesktopFeeDropDown extends ConsumerStatefulWidget { - const DesktopFeeDropDown({ - super.key, - required this.walletId, - this.isToken = false, - }); - - final String walletId; - final bool isToken; - - @override - ConsumerState createState() => _DesktopFeeDropDownState(); -} - -class _DesktopFeeDropDownState extends ConsumerState { - late final String walletId; - - FeeObject? feeObject; - FeeRateType feeRateType = FeeRateType.average; - - final stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; - - Future feeFor({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) async { - switch (feeRateType) { - case FeeRateType.fast: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .fast[amount] == - null) { - if (widget.isToken == false) { - final wallet = ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.high.value, - ); - ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; - } else if (coin is Firo) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).fast[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .fast[amount]!; - - case FeeRateType.average: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .average[amount] == - null) { - if (widget.isToken == false) { - 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) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).average[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).average[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).average[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .average[amount]!; - - case FeeRateType.slow: - if (ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .slow[amount] == - null) { - if (widget.isToken == false) { - final wallet = ref.read(pWallets).getWallet(walletId); - - if (coin is Monero || coin is Wownero) { - final fee = await wallet.estimateFeeFor( - amount, - lib_monero.TransactionPriority.normal.value, - ); - ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; - } else if (coin is Firo) { - final Amount fee; - switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); - case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); - case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); - } - ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; - } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); - } - } else { - final tokenWallet = ref.read(pCurrentTokenWallet)!; - final fee = await tokenWallet.estimateFeeFor(amount, feeRate); - ref.read(tokenFeeSessionCacheProvider).slow[amount] = fee; - } - } - return ref - .read( - widget.isToken - ? tokenFeeSessionCacheProvider - : feeSheetSessionCacheProvider, - ) - .slow[amount]!; - default: - return Amount.zero; - } - } - - @override - void initState() { - walletId = widget.walletId; - super.initState(); - } - - String? labelSlow; - String? labelAverage; - String? labelFast; - - @override - Widget build(BuildContext context) { - debugPrint("BUILD: $runtimeType"); - - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); - - return FutureBuilder( - future: wallet.fees, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - feeObject = snapshot.data!; - } - return DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - value: ref.watch(feeRateTypeStateProvider.state).state, - items: [ - ...FeeRateType.values.map( - (e) => DropdownMenuItem( - value: e, - child: FeeDropDownChild( - feeObject: feeObject, - feeRateType: e, - walletId: walletId, - feeFor: feeFor, - isSelected: false, - ), - ), - ), - ], - onChanged: (newRateType) { - if (newRateType is FeeRateType) { - ref.read(feeRateTypeStateProvider.state).state = newRateType; - } - }, - iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 12, - height: 6, - color: Theme.of(context).extension()!.textDark3, - ), - ), - dropdownStyleData: DropdownStyleData( - offset: const Offset(0, -10), - elevation: 0, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), - ); - }, - ); - } -} - -final sendAmountProvider = - StateProvider.autoDispose((_) => Amount.zero); - -class FeeDropDownChild extends ConsumerWidget { - const FeeDropDownChild({ - super.key, - required this.feeObject, - required this.feeRateType, - required this.walletId, - required this.feeFor, - required this.isSelected, - }); - - final FeeObject? feeObject; - final FeeRateType feeRateType; - final String walletId; - final Future Function({ - required Amount amount, - required FeeRateType feeRateType, - required int feeRate, - required CryptoCurrency coin, - }) feeFor; - final bool isSelected; - - static const stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; - - String estimatedTimeToBeIncludedInNextBlock( - int targetBlockTime, - int estimatedNumberOfBlocks, - ) { - final int time = targetBlockTime * estimatedNumberOfBlocks; - - final int hours = (time / 3600).floor(); - if (hours > 1) { - return "~$hours hours"; - } else if (hours == 1) { - return "~$hours hour"; - } - - // less than an hour - - final string = (time / 60).toStringAsFixed(1); - - if (string == "1.0") { - return "~1 minute"; - } else { - if (string.endsWith(".0")) { - return "~${(time / 60).floor()} minutes"; - } - return "~$string minutes"; - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: $runtimeType : $feeRateType"); - - final coin = ref.watch(pWalletCoin(walletId)); - - if (feeObject == null) { - return AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: - Theme.of(context).extension()!.textFieldActiveText, - ), - ); - } else { - return FutureBuilder( - future: feeFor( - coin: coin, - feeRateType: feeRateType, - feeRate: feeRateType == FeeRateType.fast - ? feeObject!.fast - : feeRateType == FeeRateType.slow - ? feeObject!.slow - : feeObject!.medium, - amount: ref.watch(sendAmountProvider.state).state, - ), - builder: (_, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${feeRateType.prettyName} " - "(~${ref.watch(pAmountFormatter(coin)).format( - snapshot.data!, - indicatePrecisionLoss: false, - )})", - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ), - if (feeObject != null) - Text( - coin is Ethereum - ? "" - : estimatedTimeToBeIncludedInNextBlock( - coin.targetBlockTimeSeconds, - feeRateType == FeeRateType.fast - ? feeObject!.numberOfBlocksFast - : feeRateType == FeeRateType.slow - ? feeObject!.numberOfBlocksSlow - : feeObject!.numberOfBlocksAverage, - ), - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - ], - ); - } else { - return AnimatedText( - stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - ); - } - }, - ); - } - } -} 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..300bc4d99 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 @@ -10,7 +10,6 @@ import 'dart:async'; -import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; @@ -28,7 +27,9 @@ import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet. 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/desktop_fee_providers.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'; @@ -39,7 +40,6 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../../utilities/logger.dart'; import '../../../../utilities/prefs.dart'; import '../../../../utilities/text_styles.dart'; @@ -50,20 +50,16 @@ import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/models/tx_data.dart'; import '../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; -import '../../../../widgets/animated_text.dart'; -import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; -import '../../../../widgets/desktop/desktop_fee_dialog.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/dialogs/firo_exchange_address_dialog.dart'; -import '../../../../widgets/fee_slider.dart'; +import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/qrcode_icon.dart'; @@ -74,7 +70,7 @@ import '../../../../widgets/textfield_icon_button.dart'; import '../../../coin_control/desktop_coin_control_use_dialog.dart'; import '../../../desktop_home_view.dart'; import 'address_book_address_chooser/address_book_address_chooser.dart'; -import 'desktop_fee_dropdown.dart'; +import 'desktop_send_fee_form.dart'; class DesktopSend extends ConsumerStatefulWidget { const DesktopSend({ @@ -105,8 +101,8 @@ class _DesktopSendState extends ConsumerState { late TextEditingController sendToController; late TextEditingController cryptoAmountController; late TextEditingController baseAmountController; - // late TextEditingController feeController; late TextEditingController memoController; + late TextEditingController nonceController; late final SendViewAutoFillData? _data; @@ -114,6 +110,7 @@ class _DesktopSendState extends ConsumerState { final _cryptoFocus = FocusNode(); final _baseFocus = FocusNode(); final _memoFocus = FocusNode(); + final _nonceFocusNode = FocusNode(); late final bool isStellar; @@ -134,14 +131,7 @@ class _DesktopSendState extends ConsumerState { bool isCustomFee = false; int customFeeRate = 1; - (FeeRateType, String?, String?)? feeSelectionResult; - - final stringsToLoopThrough = [ - "Calculating", - "Calculating.", - "Calculating..", - "Calculating...", - ]; + EthEIP1559Fee? ethFee; Future scanWebcam() async { try { @@ -155,13 +145,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, + ); } } @@ -190,9 +186,7 @@ class _DesktopSendState extends ConsumerState { ref.read(prefsChangeNotifierProvider).enableCoinControl; if (!(wallet is CoinControlInterface && coinControlEnabled) || - (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isEmpty)) { + (coinControlEnabled && ref.read(desktopUseUTXOs).isEmpty)) { // confirm send all if (amount == availableBalance) { final bool? shouldSendAll = await showDialog( @@ -204,10 +198,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 +212,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 +237,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -301,7 +281,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 +300,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; @@ -331,7 +308,7 @@ class _DesktopSendState extends ConsumerState { if (isPaynymSend) { final paynymWallet = wallet as PaynymInterface; - final feeRate = ref.read(feeRateTypeStateProvider); + final feeRate = ref.read(feeRateTypeDesktopStateProvider); txDataFuture = paynymWallet.preparePaymentCodeSend( txData: TxData( paynymAccountLite: widget.accountLite!, @@ -344,11 +321,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) { @@ -365,32 +343,28 @@ class _DesktopSendState extends ConsumerState { isChange: false, ), ], - feeRateType: ref.read(feeRateTypeStateProvider), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (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), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + utxos: + (coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, ), ); } @@ -400,11 +374,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 +383,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 +408,26 @@ 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), + feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, - utxos: (wallet is CoinControlInterface && - coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) - : null, + nonce: + wallet.cryptoCurrency is Ethereum + ? int.tryParse(nonceController.text) + : null, + utxos: + (wallet is CoinControlInterface && + coinControlEnabled && + ref.read(desktopUseUTXOs).isNotEmpty) + ? ref.read(desktopUseUTXOs) + : null, + ethEIP1559Fee: ethFee, ), ); } - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; @@ -473,35 +438,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 +468,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 +478,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 +492,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 +518,7 @@ class _DesktopSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), ], ), ], @@ -593,6 +537,7 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.text = ""; baseAmountController.text = ""; memoController.text = ""; + nonceController.text = ""; _address = ""; _addressToggleFlag = false; if (mounted) { @@ -602,9 +547,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; @@ -615,9 +560,9 @@ class _DesktopSendState extends ConsumerState { _cachedAmountToSend = amount; final price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { final String fiatAmountString = (amount.decimal * price) .toAmount(fractionDigits: 2) .fiatString( @@ -685,14 +630,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 +651,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 +671,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 +690,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 +736,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 +755,7 @@ class _DesktopSendState extends ConsumerState { content = AddressUtils().formatAddress(content); } - sendToController.text = content; - _address = content; + await _checkSparkNameAndOrSetAddress(content); // Trigger validation after pasting. _setValidAddressProviders(_address); @@ -818,26 +791,26 @@ class _DesktopSendState extends ConsumerState { final Amount? amount; if (baseAmount != null) { final _price = - ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin)?.value; - if (_price == Decimal.zero) { + if (_price == null || _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 +838,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 +863,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, + ), ); } @@ -929,7 +901,7 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController = TextEditingController(); baseAmountController = TextEditingController(); memoController = TextEditingController(); - // feeController = TextEditingController(); + nonceController = TextEditingController(); onCryptoAmountChanged = _cryptoAmountChanged; cryptoAmountController.addListener(onCryptoAmountChanged); @@ -983,12 +955,13 @@ class _DesktopSendState extends ConsumerState { cryptoAmountController.dispose(); baseAmountController.dispose(); memoController.dispose(); - // feeController.dispose(); + nonceController.dispose(); _addressFocusNode.dispose(); _cryptoFocus.dispose(); _baseFocus.dispose(); _memoFocus.dispose(); + _nonceFocusNode.dispose(); super.dispose(); } @@ -1033,7 +1006,8 @@ class _DesktopSendState extends ConsumerState { } }); - final showCoinControl = ref.watch( + final showCoinControl = + ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), @@ -1044,23 +1018,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 +1045,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 +1069,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 +1091,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 +1127,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 +1165,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 +1189,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 +1199,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 +1229,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 +1241,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 +1262,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 +1288,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 +1299,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 +1328,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 +1373,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 +1401,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 +1420,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 +1515,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 +1536,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 +1577,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 +1596,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 +1618,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 +1638,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 +1649,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,245 +1661,69 @@ class _DesktopSendState extends ConsumerState { ), ), ), - 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 && - !(((coin is Firo) && - (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.lelantus || - ref - .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(() {}); - }, - ), - ], - ), - child: Text( - "Transaction fee" - "${isCustomFee ? "" : " (${coin is Ethereum ? "max" : "estimated"})"}", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - textAlign: TextAlign.left, - ), - ), + if (!isPaynymSend) const SizedBox(height: 20), if (coin is! NanoCurrency && coin is! Epiccash && coin is! Tezos) - const SizedBox( - height: 10, + DesktopSendFeeForm( + walletId: walletId, + isToken: false, + onCustomFeeSliderChanged: (value) => customFeeRate = value, + onCustomFeeOptionChanged: (value) { + isCustomFee = value; + ethFee = null; + }, + onCustomEip1559FeeOptionChanged: (value) => ethFee = value, ), - 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, - ), - ), - 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.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 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, - ), - ), - ], - ); - } - }, - ) - : (coin is Firo) && - ref - .watch( - publicPrivateBalanceStateProvider.state, - ) - .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( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, - ), - textAlign: TextAlign.left, - ), - Text( - feeSelectionResult?.$3 ?? "", - style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - ), - ], - ), + if (coin is Ethereum) const SizedBox(height: 20), + if (coin is Ethereum) + Text( + "Nonce", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), - if (isCustomFee) - Padding( - padding: const EdgeInsets.only( - bottom: 12, - top: 16, + textAlign: TextAlign.left, + ), + if (coin is Ethereum) const SizedBox(height: 10), + if (coin is Ethereum) + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - child: FeeSlider( - coin: coin, - onSatVByteChanged: (rate) { - customFeeRate = rate; - }, + child: TextField( + minLines: 1, + maxLines: 1, + key: const Key("sendViewNonceFieldKey"), + controller: nonceController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(), + focusNode: _nonceFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Leave empty to auto select nonce", + _nonceFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + ), ), ), - 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_send_fee_form.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart new file mode 100644 index 000000000..154955148 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send_fee_form.dart @@ -0,0 +1,342 @@ +import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import '../../../../providers/providers.dart'; +import '../../../../providers/wallet/desktop_fee_providers.dart'; +import '../../../../providers/wallet/public_private_balance_state_provider.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/amount/amount_formatter.dart'; +import '../../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../../utilities/eth_commons.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../widgets/animated_text.dart'; +import '../../../../widgets/conditional_parent.dart'; +import '../../../../widgets/custom_buttons/blue_text_button.dart'; +import '../../../../widgets/desktop/desktop_fee_dialog.dart'; +import '../../../../widgets/eth_fee_form.dart'; +import '../../../../widgets/fee_slider.dart'; + +class DesktopSendFeeForm extends ConsumerStatefulWidget { + const DesktopSendFeeForm({ + super.key, + required this.walletId, + required this.isToken, + required this.onCustomFeeSliderChanged, + required this.onCustomFeeOptionChanged, + this.onCustomEip1559FeeOptionChanged, + }); + + final String walletId; + final bool isToken; + final void Function(int) onCustomFeeSliderChanged; + final void Function(bool) onCustomFeeOptionChanged; + final void Function(EthEIP1559Fee)? onCustomEip1559FeeOptionChanged; + + @override + ConsumerState createState() => _DesktopSendFeeFormState(); +} + +class _DesktopSendFeeFormState extends ConsumerState { + final stringsToLoopThrough = [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ]; + + late final CryptoCurrency cryptoCurrency; + + bool get isEth => cryptoCurrency is Ethereum; + + bool isCustomFee = false; + (FeeRateType, String?, String?)? feeSelectionResult; + + @override + void initState() { + super.initState(); + cryptoCurrency = ref.read(pWalletCoin(widget.walletId)); + } + + @override + Widget build(BuildContext context) { + final canEditFees = + isEth || + (cryptoCurrency is ElectrumXCurrencyInterface && + !(((cryptoCurrency is Firo) && + (ref.watch(publicPrivateBalanceStateProvider.state).state == + FiroType.lelantus || + ref.watch(publicPrivateBalanceStateProvider.state).state == + FiroType.spark)))); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: canEditFees, + builder: + (child) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + child, + CustomTextButton( + text: "Edit", + onTap: () async { + feeSelectionResult = + await showDialog<(FeeRateType, String?, String?)?>( + context: context, + builder: + (_) => DesktopFeeDialog( + walletId: widget.walletId, + isToken: widget.isToken, + ), + ); + + 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 ? "" : " (${isEth ? "max" : "estimated"})"}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + ), + const SizedBox(height: 10), + if (!isCustomFee) + Padding( + padding: const EdgeInsets.all(10), + child: + (feeSelectionResult?.$2 == null) + ? FutureBuilder( + future: ref.watch( + pWallets.select( + (value) => value.getWallet(widget.walletId).fees, + ), + ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return DesktopFeeItem( + feeObject: snapshot.data, + feeRateType: FeeRateType.average, + walletId: widget.walletId, + isButton: false, + feeFor: ({ + required Amount amount, + required FeeRateType feeRateType, + required BigInt feeRate, + required CryptoCurrency coin, + }) async { + if (ref + .read( + widget.isToken + ? tokenFeeSessionCacheProvider + : feeSheetSessionCacheProvider, + ) + .average[amount] == + null) { + if (widget.isToken == false) { + final wallet = ref + .read(pWallets) + .getWallet(widget.walletId); + + if (coin is Monero || coin is Wownero) { + final fee = await wallet.estimateFeeFor( + amount, + BigInt.from( + 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.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 wallet + .estimateFeeFor(amount, feeRate); + } + } else { + final tokenWallet = + ref.read(pCurrentTokenWallet)!; + final fee = await tokenWallet.estimateFeeFor( + amount, + feeRate, + ); + ref + .read(tokenFeeSessionCacheProvider) + .average[amount] = + fee; + } + } + return ref + .read( + widget.isToken + ? tokenFeeSessionCacheProvider + : feeSheetSessionCacheProvider, + ) + .average[amount]!; + }, + isSelected: true, + ); + } else { + return Row( + children: [ + AnimatedText( + stringsToLoopThrough: stringsToLoopThrough, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveText, + ), + ), + ], + ); + } + }, + ) + : (cryptoCurrency is Firo) && + ref + .watch(publicPrivateBalanceStateProvider.state) + .state == + FiroType.lelantus + ? Builder( + builder: (context) { + final lelantusFee = ref + .watch(pAmountFormatter(cryptoCurrency)) + .format( + Amount( + rawValue: BigInt.parse("3794"), + fractionDigits: cryptoCurrency.fractionDigits, + ), + indicatePrecisionLoss: false, + ); + return Text( + "~$lelantusFee", + 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( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + ), + textAlign: TextAlign.left, + ), + Text( + feeSelectionResult?.$3 ?? "", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + ), + ], + ), + ), + if (isCustomFee && isEth) + EthFeeForm( + minGasLimit: + widget.isToken + ? kEthereumTokenMinGasLimit + : kEthereumMinGasLimit, + stateChanged: + (value) => widget.onCustomEip1559FeeOptionChanged?.call(value), + ), + if (isCustomFee && !isEth) + Padding( + padding: const EdgeInsets.only(bottom: 12, top: 16), + child: FeeSlider( + coin: cryptoCurrency, + onSatVByteChanged: widget.onCustomFeeSliderChanged, + ), + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart index 647b1e1cc..48e8cb230 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -23,6 +23,7 @@ import '../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart 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/desktop_fee_providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; import '../../../../utilities/amount/amount.dart'; @@ -33,7 +34,6 @@ import '../../../../utilities/barcode_scanner_interface.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/logger.dart'; -import '../../../../utilities/prefs.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -45,6 +45,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/desktop/secondary_button.dart'; +import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../../widgets/icon_widgets/clipboard_icon.dart'; import '../../../../widgets/icon_widgets/x_icon.dart'; @@ -52,9 +53,7 @@ import '../../../../widgets/stack_text_field.dart'; import '../../../../widgets/textfield_icon_button.dart'; import '../../../desktop_home_view.dart'; import 'address_book_address_chooser/address_book_address_chooser.dart'; -import 'desktop_fee_dropdown.dart'; - -// const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; +import 'desktop_send_fee_form.dart'; class DesktopTokenSend extends ConsumerStatefulWidget { const DesktopTokenSend({ @@ -105,20 +104,21 @@ class _DesktopTokenSendState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; + EthEIP1559Fee? ethFee; + Future previewSend() async { final tokenWallet = ref.read(pCurrentTokenWallet)!; final Amount amount = _amountToSend!; - final Amount availableBalance = ref - .read( - pTokenBalance( - ( - walletId: walletId, - contractAddress: tokenWallet.tokenContract.address - ), - ), - ) - .spendable; + final Amount availableBalance = + ref + .read( + pTokenBalance(( + walletId: walletId, + contractAddress: tokenWallet.tokenContract.address, + )), + ) + .spendable; // confirm send all if (amount == availableBalance) { @@ -131,10 +131,7 @@ class _DesktopTokenSendState 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: [ @@ -148,29 +145,20 @@ class _DesktopTokenSendState 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( @@ -182,9 +170,7 @@ class _DesktopTokenSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Expanded( child: PrimaryButton( buttonHeight: ButtonHeight.l, @@ -241,71 +227,52 @@ class _DesktopTokenSendState extends ConsumerState { ); } - final time = Future.delayed( - const Duration( - milliseconds: 2500, - ), - ); + final time = Future.delayed(const Duration(milliseconds: 2500)); TxData txData; Future txDataFuture; txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [ - ( - address: _address!, - amount: amount, - isChange: false, - ), - ], - feeRateType: ref.read(feeRateTypeStateProvider), + recipients: [(address: _address!, amount: amount, isChange: false)], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), nonce: int.tryParse(nonceController.text), + ethEIP1559Fee: ethFee, ), ); - final results = await Future.wait([ - txDataFuture, - time, - ]); + final results = await Future.wait([txDataFuture, time]); txData = results.first as TxData; if (!wasCancelled && mounted) { - txData = txData.copyWith( - note: _note ?? "", - ); + txData = txData.copyWith(note: _note ?? ""); // 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, - isTokenTx: true, - routeOnSuccessName: DesktopHomeView.routeName, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmTransactionView( + txData: txData, + walletId: walletId, + onSuccess: clearSendForm, + isTokenTx: true, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), ), ); } } catch (e) { if (mounted) { // pop building dialog - Navigator.of( - context, - rootNavigator: true, - ).pop(); + Navigator.of(context, rootNavigator: true).pop(); unawaited( showDialog( @@ -315,10 +282,7 @@ class _DesktopTokenSendState 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: [ @@ -332,25 +296,18 @@ class _DesktopTokenSendState 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: SelectableText( 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( @@ -365,9 +322,7 @@ class _DesktopTokenSendState extends ConsumerState { }, ), ), - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), ], ), ], @@ -395,7 +350,9 @@ class _DesktopTokenSendState extends ConsumerState { void _cryptoAmountChanged() async { if (!_cryptoAmountChangeLock) { - final cryptoAmount = ref.read(pAmountFormatter(coin)).tryParse( + final cryptoAmount = ref + .read(pAmountFormatter(coin)) + .tryParse( cryptoAmountController.text, ethContract: ref.read(pCurrentTokenWallet)!.tokenContract, ); @@ -408,14 +365,15 @@ class _DesktopTokenSendState extends ConsumerState { } _cachedAmountToSend = _amountToSend; - final price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice( - ref.read(pCurrentTokenWallet)!.tokenContract.address, - ) - .item1; + final price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + ref.read(pCurrentTokenWallet)!.tokenContract.address, + ) + ?.value; - if (price > Decimal.zero) { + if (price != null && price > Decimal.zero) { final String fiatAmountString = Amount.fromDecimal( _amountToSend!.decimal * price, fractionDigits: 2, @@ -495,10 +453,9 @@ class _DesktopTokenSendState extends ConsumerState { fractionDigits: ref.read(pCurrentTokenWallet)!.tokenContract.decimals, ); - cryptoAmountController.text = ref.read(pAmountFormatter(coin)).format( - amount, - withUnitName: false, - ); + cryptoAmountController.text = ref + .read(pAmountFormatter(coin)) + .format(amount, withUnitName: false); _amountToSend = amount; } @@ -554,33 +511,39 @@ class _DesktopTokenSendState extends ConsumerState { if (baseAmountString.isNotEmpty && baseAmountString != "." && baseAmountString != ",") { - final baseAmount = baseAmountString.contains(",") - ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - .toAmount(fractionDigits: 2) - : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); - - final Decimal _price = ref - .read(priceAnd24hChangeNotifierProvider) - .getTokenPrice( - ref.read(pCurrentTokenWallet)!.tokenContract.address, - ) - .item1; - - if (_price == Decimal.zero) { + final baseAmount = + baseAmountString.contains(",") + ? Decimal.parse( + baseAmountString.replaceFirst(",", "."), + ).toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); + + final Decimal? _price = + ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + ref.read(pCurrentTokenWallet)!.tokenContract.address, + ) + ?.value; + + if (_price == null || _price == Decimal.zero) { _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); } else { - _amountToSend = baseAmount <= Amount.zero - ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) - : (baseAmount.decimal / _price) - .toDecimal(scaleOnInfinitePrecision: tokenDecimals) - .toAmount(fractionDigits: tokenDecimals); + _amountToSend = + baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: tokenDecimals) + .toAmount(fractionDigits: tokenDecimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; } _cachedAmountToSend = _amountToSend; - final amountString = ref.read(pAmountFormatter(coin)).format( + final amountString = ref + .read(pAmountFormatter(coin)) + .format( _amountToSend!, withUnitName: false, ethContract: ref.read(pCurrentTokenWallet)!.tokenContract, @@ -602,19 +565,15 @@ class _DesktopTokenSendState extends ConsumerState { Future sendAllTapped() async { cryptoAmountController.text = ref .read( - pTokenBalance( - ( - walletId: walletId, - contractAddress: - ref.read(pCurrentTokenWallet)!.tokenContract.address - ), - ), + pTokenBalance(( + walletId: walletId, + contractAddress: + ref.read(pCurrentTokenWallet)!.tokenContract.address, + )), ) .spendable .decimal - .toStringAsFixed( - ref.read(pCurrentTokenWallet)!.tokenContract.decimals, - ); + .toStringAsFixed(ref.read(pCurrentTokenWallet)!.tokenContract.decimals); } @override @@ -702,16 +661,15 @@ class _DesktopTokenSendState 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, ), @@ -721,9 +679,10 @@ class _DesktopTokenSendState extends ConsumerState { Text( "Amount", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), @@ -733,9 +692,7 @@ class _DesktopTokenSendState extends ConsumerState { ), ], ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -745,20 +702,22 @@ class _DesktopTokenSendState 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( decimals: tokenContract.decimals, unit: ref.watch(pAmountUnit(coin)), locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ), // regex to validate a crypto amount with 8 decimal places @@ -780,9 +739,10 @@ class _DesktopTokenSendState 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, @@ -791,20 +751,23 @@ class _DesktopTokenSendState extends ConsumerState { child: Text( ref.watch(pAmountUnit(coin)).unitForContract(tokenContract), 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) + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) + const SizedBox(height: 10), + if (ref.watch( + prefsChangeNotifierProvider.select((s) => s.externalCalls), + )) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -814,19 +777,21 @@ class _DesktopTokenSendState 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: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), ), ), // // regex to validate a fiat amount with 2 decimal places @@ -845,9 +810,10 @@ class _DesktopTokenSendState 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, @@ -855,34 +821,33 @@ class _DesktopTokenSendState 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, ), ), ), ), ), ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text( "Send to", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -915,9 +880,10 @@ class _DesktopTokenSendState 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( @@ -933,78 +899,83 @@ class _DesktopTokenSendState 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( - "sendTokenViewClearAddressFieldButtonKey", - ), - onTap: () { - sendToController.text = ""; - _address = ""; - _updatePreviewButtonState( - _address, - _amountToSend, - ); - setState(() { - _addressToggleFlag = false; - }); - }, - child: const XIcon(), - ) + key: const Key( + "sendTokenViewClearAddressFieldButtonKey", + ), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, + _amountToSend, + ); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) : TextFieldIconButton( - key: const Key( - "sendTokenViewPasteAddressFieldButtonKey", - ), - onTap: pasteAddress, - child: sendToController.text.isEmpty - ? const ClipboardIcon() - : const XIcon(), + key: const Key( + "sendTokenViewPasteAddressFieldButtonKey", ), + onTap: pasteAddress, + child: + sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), if (sendToController.text.isEmpty) TextFieldIconButton( key: const Key("sendTokenViewAddressBookButtonKey"), 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) { @@ -1034,9 +1005,7 @@ class _DesktopTokenSendState extends ConsumerState { ), Builder( builder: (_) { - final error = _updateInvalidAddressText( - _address ?? "", - ); + final error = _updateInvalidAddressText(_address ?? ""); if (error == null || error.isEmpty) { return Container(); @@ -1044,10 +1013,7 @@ class _DesktopTokenSendState 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, @@ -1061,40 +1027,28 @@ class _DesktopTokenSendState extends ConsumerState { } }, ), - const SizedBox( - height: 20, - ), - Text( - "Transaction fee (max)", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, - ), - textAlign: TextAlign.left, - ), - const SizedBox( - height: 10, - ), - DesktopFeeDropDown( + const SizedBox(height: 20), + DesktopSendFeeForm( walletId: walletId, isToken: true, + onCustomFeeSliderChanged: (value) => {}, + onCustomFeeOptionChanged: (value) { + ethFee = null; + }, + onCustomEip1559FeeOptionChanged: (value) => ethFee = value, ), - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), Text( "Nonce", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + color: + Theme.of( + context, + ).extension()!.textFieldActiveSearchIconRight, ), textAlign: TextAlign.left, ), - const SizedBox( - height: 10, - ), + const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -1110,9 +1064,10 @@ class _DesktopTokenSendState extends ConsumerState { keyboardType: const TextInputType.numberWithOptions(), focusNode: _nonceFocusNode, style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, height: 1.8, ), decoration: standardInputDecoration( @@ -1130,16 +1085,15 @@ class _DesktopTokenSendState extends ConsumerState { ), ), ), - const SizedBox( - height: 36, - ), + const SizedBox(height: 36), PrimaryButton( buttonHeight: ButtonHeight.l, label: "Preview send", enabled: ref.watch(previewTokenTxButtonStateProvider.state).state, - onPressed: ref.watch(previewTokenTxButtonStateProvider.state).state - ? previewSend - : null, + onPressed: + ref.watch(previewTokenTxButtonStateProvider.state).state + ? previewSend + : null, ), ], ); 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/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index fdc0f99b7..dc09c757d 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -84,7 +84,7 @@ class _WDesktopWalletSummaryState extends ConsumerState { ) : null; - final priceTuple = + final price = widget.isToken ? ref.watch( priceAnd24hChangeNotifierProvider.select( @@ -150,9 +150,9 @@ class _WDesktopWalletSummaryState extends ConsumerState { style: STextStyles.desktopH3(context), ), ), - if (externalCalls) + if (externalCalls && price != null) SelectableText( - "${Amount.fromDecimal(priceTuple.item1 * balanceToShow.decimal, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", + "${Amount.fromDecimal(price.value * balanceToShow.decimal, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart index 73f384f82..ecc80aa28 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/firo_desktop_wallet_summary.dart @@ -66,12 +66,14 @@ class _WFiroDesktopWalletSummaryState if (ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls), )) { - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ); - price = priceTuple.item1; + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; } final _showAvailable = 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/providers/ui/fee_rate_type_state_provider.dart b/lib/providers/ui/fee_rate_type_state_provider.dart index 3e1421c14..8b309a2e5 100644 --- a/lib/providers/ui/fee_rate_type_state_provider.dart +++ b/lib/providers/ui/fee_rate_type_state_provider.dart @@ -9,7 +9,13 @@ */ import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../utilities/enums/fee_rate_type_enum.dart'; -final feeRateTypeStateProvider = - StateProvider.autoDispose((_) => FeeRateType.average); +final feeRateTypeMobileStateProvider = StateProvider.autoDispose( + (_) => FeeRateType.average, +); + +final feeRateTypeDesktopStateProvider = StateProvider( + (_) => FeeRateType.average, +); diff --git a/lib/providers/wallet/desktop_fee_providers.dart b/lib/providers/wallet/desktop_fee_providers.dart new file mode 100644 index 000000000..d1723c78c --- /dev/null +++ b/lib/providers/wallet/desktop_fee_providers.dart @@ -0,0 +1,23 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import '../../utilities/amount/amount.dart'; + +final tokenFeeSessionCacheProvider = + ChangeNotifierProvider((ref) { + return FeeSheetSessionCache(); + }); + +final sendAmountProvider = StateProvider.autoDispose( + (_) => Amount.zero, +); 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/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index fccce0ceb..091165b30 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -10,18 +10,13 @@ import 'dart:convert'; -import 'package:tuple/tuple.dart'; - import '../../dto/ethereum/eth_token_tx_dto.dart'; -import '../../dto/ethereum/eth_token_tx_extra_dto.dart'; import '../../dto/ethereum/eth_tx_dto.dart'; -import '../../dto/ethereum/pending_eth_tx_dto.dart'; import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../models/paymint/fee_object_model.dart'; import '../../networking/http.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/eth_commons.dart'; -import '../../utilities/extensions/extensions.dart'; import '../../utilities/logger.dart'; import '../../utilities/prefs.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; @@ -60,11 +55,12 @@ abstract class EthereumAPI { try { final response = await client.get( url: Uri.parse( - "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock", + "$stackBaseServer/export?addrs=$address&firstBlock=$firstBlock&unripe=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -80,17 +76,11 @@ abstract class EthereumAPI { txns.add(txn); } } - return EthereumResponse( - txns, - null, - ); + return EthereumResponse(txns, null); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); + return EthereumResponse([], null); } } else { throw EthApiException( @@ -99,10 +89,7 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e("getEthTransactions()", error: e); Logging.instance.d( @@ -110,212 +97,7 @@ abstract class EthereumAPI { error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future> getEthTransactionByHash( - String txid, - ) async { - try { - final response = await client.post( - url: Uri.parse( - "$stackBaseServer/v1/mainnet", - ), - headers: {'Content-Type': 'application/json'}, - body: json.encode({ - "jsonrpc": "2.0", - "method": "eth_getTransactionByHash", - "params": [ - txid, - ], - "id": DateTime.now().millisecondsSinceEpoch, - }), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - try { - final json = jsonDecode(response.body) as Map; - final result = json["result"] as Map; - return EthereumResponse( - PendingEthTxDto.fromMap(Map.from(result)), - null, - ); - } catch (_) { - throw EthApiException( - "getEthTransactionByHash($txid) failed with response: " - "${response.body}", - ); - } - } else { - throw EthApiException( - "getEthTransactionByHash($txid) response is empty but status code is " - "${response.code}", - ); - } - } else { - throw EthApiException( - "getEthTransactionByHash($txid) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTransactionByHash()", - error: e, - ); - Logging.instance.d( - "getEthTransactionByHash($txid)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future>>> - getEthTransactionNonces( - List txns, - ) async { - try { - final response = await client.get( - url: Uri.parse( - "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - final json = jsonDecode(response.body) as Map; - final list = List>.from(json["data"] as List); - - final List> result = []; - - for (final dto in txns) { - final data = - list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); - - final nonce = (data["nonce"] as String?)?.toBigIntFromHex.toInt(); - result.add(Tuple2(dto, nonce)); - } - return EthereumResponse( - result, - null, - ); - } else { - // nice that the api returns an empty body instead of being - // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); - } - } else { - throw EthApiException( - "getEthTransactionNonces($txns) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTransactionNonces()", - error: e, - ); - Logging.instance.d( - "getEthTransactionNonces($txns)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future>> - getEthTokenTransactionsByTxids(List txids) async { - try { - final response = await client.get( - url: Uri.parse( - "$stackBaseServer/transactions?transactions=${txids.join(" ")}", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - if (response.body.isNotEmpty) { - final json = jsonDecode(response.body) as Map; - final list = json["data"] as List?; - - final List txns = []; - for (final map in list!) { - final txn = EthTokenTxExtraDTO.fromMap( - Map.from(map as Map), - ); - - txns.add(txn); - } - return EthereumResponse( - txns, - null, - ); - } else { - throw EthApiException( - "getEthTokenTransactionsByTxids($txids) response is empty but status code is " - "${response.code}", - ); - } - } else { - throw EthApiException( - "getEthTokenTransactionsByTxids($txids) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getEthTokenTransactionsByTxids()", - error: e, - ); - Logging.instance.d( - "getEthTokenTransactionsByTxids($txids)", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -328,9 +110,10 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -340,22 +123,17 @@ abstract class EthereumAPI { final List txns = []; for (final map in list!) { - final txn = - EthTokenTxDto.fromMap(Map.from(map as Map)); + final txn = EthTokenTxDto.fromMap( + Map.from(map as Map), + ); txns.add(txn); } - return EthereumResponse( - txns, - null, - ); + return EthereumResponse(txns, null); } else { // nice that the api returns an empty body instead of being // consistent and returning a json object with no transactions - return EthereumResponse( - [], - null, - ); + return EthereumResponse([], null); } } else { throw EthApiException( @@ -364,100 +142,18 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getTokenTransactions()", - error: e, - ); + Logging.instance.e("getTokenTransactions()", error: e); Logging.instance.d( "getTokenTransactions($address, $tokenContractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } -// ONLY FETCHES WALLET TOKENS WITH A NON ZERO BALANCE - // static Future>> getWalletTokens({ - // required String address, - // }) async { - // try { - // final uri = Uri.parse( - // "$blockExplorer?module=account&action=tokenlist&address=$address", - // ); - // final response = await get(uri); - // - // if (response.statusCode == 200) { - // final json = jsonDecode(response.body); - // if (json["message"] == "OK") { - // final result = - // List>.from(json["result"] as List); - // final List tokens = []; - // for (final map in result) { - // if (map["type"] == "ERC-20") { - // tokens.add( - // Erc20Token( - // balance: int.parse(map["balance"] as String), - // contractAddress: map["contractAddress"] as String, - // decimals: int.parse(map["decimals"] as String), - // name: map["name"] as String, - // symbol: map["symbol"] as String, - // ), - // ); - // } else if (map["type"] == "ERC-721") { - // tokens.add( - // Erc721Token( - // balance: int.parse(map["balance"] as String), - // contractAddress: map["contractAddress"] as String, - // decimals: int.parse(map["decimals"] as String), - // name: map["name"] as String, - // symbol: map["symbol"] as String, - // ), - // ); - // } else { - // throw EthApiException( - // "Unsupported token type found: ${map["type"]}"); - // } - // } - // - // return EthereumResponse( - // tokens, - // null, - // ); - // } else { - // throw EthApiException(json["message"] as String); - // } - // } else { - // throw EthApiException( - // "getWalletTokens($address) failed with status code: " - // "${response.statusCode}", - // ); - // } - // } on EthApiException catch (e) { - // return EthereumResponse( - // null, - // e, - // ); - // } catch (e, s) { - // Logging.instance.log( - // "getWalletTokens(): $e\n$s", - // level: LogLevel.Error, - // ); - // return EthereumResponse( - // null, - // EthApiException(e.toString()), - // ); - // } - // } - static Future> getWalletTokenBalance({ required String address, required String contractAddress, @@ -468,9 +164,10 @@ abstract class EthereumAPI { ); final response = await client.get( url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -479,7 +176,7 @@ abstract class EthereumAPI { final map = json["data"].first as Map; final balance = - BigInt.tryParse(map["units"].toString()) ?? BigInt.zero; + BigInt.tryParse(map["balance"].toString()) ?? BigInt.zero; return EthereumResponse( Amount(rawValue: balance, fractionDigits: map["decimals"] as int), @@ -495,84 +192,21 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); - } catch (e, s) { - Logging.instance.e( - "getWalletTokenBalance()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); - } - } - - static Future> getAddressNonce({ - required String address, - }) async { - try { - final uri = Uri.parse( - "$stackBaseServer/state?addrs=$address&parts=all", - ); - final response = await client.get( - url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - - if (response.code == 200) { - final json = jsonDecode(response.body); - if (json["data"] is List) { - final map = json["data"].first as Map; - - final nonce = map["nonce"] as int; - - return EthereumResponse( - nonce, - null, - ); - } else { - throw EthApiException(json["message"] as String); - } - } else { - throw EthApiException( - "getAddressNonce($address) failed with status code: " - "${response.code}", - ); - } - } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getAddressNonce()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + Logging.instance.e("getWalletTokenBalance()", error: e, stackTrace: s); + return EthereumResponse(null, EthApiException(e.toString())); } } static Future> getGasOracle() async { try { final response = await client.get( - url: Uri.parse( - "$stackBaseServer/gas-prices", - ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + url: Uri.parse("$stackBaseServer/gas-prices"), + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -604,36 +238,27 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { - Logging.instance.e( - "getGasOracle()", - error: e, - stackTrace: s, - ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + Logging.instance.e("getGasOracle()", error: e, stackTrace: s); + return EthereumResponse(null, EthApiException(e.toString())); } } - static Future getFees() async { - final fees = (await getGasOracle()).value!; - final feesFast = fees.fast.shift(9).toBigInt(); - final feesStandard = fees.average.shift(9).toBigInt(); - final feesSlow = fees.slow.shift(9).toBigInt(); - - return FeeObject( - numberOfBlocksFast: fees.numberOfBlocksFast, - numberOfBlocksAverage: fees.numberOfBlocksAverage, - numberOfBlocksSlow: fees.numberOfBlocksSlow, - fast: feesFast.toInt(), - medium: feesStandard.toInt(), - slow: feesSlow.toInt(), + static Future getFees() async { + final response = await getGasOracle(); + if (response.exception != null) { + throw response.exception!; + } + + return EthFeeObject( + suggestBaseFee: response.value!.suggestBaseFee.shift(9).toBigInt(), + numberOfBlocksFast: response.value!.numberOfBlocksFast, + numberOfBlocksAverage: response.value!.numberOfBlocksAverage, + numberOfBlocksSlow: response.value!.numberOfBlocksSlow, + fast: response.value!.high.shift(9).toBigInt(), + medium: response.value!.average.shift(9).toBigInt(), + slow: response.value!.low.shift(9).toBigInt(), ); } @@ -642,9 +267,10 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/names?terms=$contractAddress&autoname=$contractAddress&all", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); } @@ -658,9 +284,10 @@ abstract class EthereumAPI { // "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", "$stackBaseServer/names?terms=$contractAddress&all", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { @@ -713,10 +340,7 @@ abstract class EthereumAPI { ); } - return EthereumResponse( - token, - null, - ); + return EthereumResponse(token, null); } else { throw EthApiException(response.body); } @@ -727,20 +351,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getTokenByContractAddress()", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -753,18 +371,16 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { final json = jsonDecode(response.body)["data"] as List; - return EthereumResponse( - jsonEncode(json), - null, - ); + return EthereumResponse(jsonEncode(json), null); } else { throw EthApiException( "getTokenAbi($name, $contractAddress) failed with status code: " @@ -772,20 +388,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getTokenAbi($name, $contractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } @@ -798,19 +408,17 @@ abstract class EthereumAPI { url: Uri.parse( "$stackBaseServer/state?addrs=$contractAddress&parts=proxy", ), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); if (response.code == 200) { final json = jsonDecode(response.body); final list = json["data"] as List; final map = Map.from(list.first as Map); - return EthereumResponse( - map["proxy"] as String, - null, - ); + return EthereumResponse(map["proxy"] as String, null); } else { throw EthApiException( "getProxyTokenImplementationAddress($contractAddress) failed with" @@ -818,20 +426,14 @@ abstract class EthereumAPI { ); } } on EthApiException catch (e) { - return EthereumResponse( - null, - e, - ); + return EthereumResponse(null, e); } catch (e, s) { Logging.instance.e( "getProxyTokenImplementationAddress($contractAddress)", error: e, stackTrace: s, ); - return EthereumResponse( - null, - EthApiException(e.toString()), - ); + return EthereumResponse(null, EthApiException(e.toString())); } } } diff --git a/lib/services/exchange/change_now/change_now_api.dart b/lib/services/exchange/change_now/change_now_api.dart index d7cf255d5..7b5cd0737 100644 --- a/lib/services/exchange/change_now/change_now_api.dart +++ b/lib/services/exchange/change_now/change_now_api.dart @@ -12,18 +12,14 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:tuple/tuple.dart'; import '../../../exceptions/exchange/exchange_exception.dart'; import '../../../exceptions/exchange/pair_unavailable_exception.dart'; -import '../../../exceptions/exchange/unsupported_currency_exception.dart'; import '../../../external_api_keys.dart'; -import '../../../models/exchange/change_now/cn_exchange_estimate.dart'; +import '../../../models/exchange/change_now/cn_exchange_transaction.dart'; +import '../../../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../../../models/exchange/change_now/estimated_exchange_amount.dart'; -import '../../../models/exchange/change_now/exchange_transaction.dart'; -import '../../../models/exchange/change_now/exchange_transaction_status.dart'; import '../../../models/exchange/response_objects/estimate.dart'; -import '../../../models/exchange/response_objects/fixed_rate_market.dart'; import '../../../models/exchange/response_objects/range.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; @@ -34,10 +30,25 @@ import '../../tor_service.dart'; import '../exchange_response.dart'; import 'change_now_exchange.dart'; +enum CNFlow { + standard("standard"), + fixedRate("fixed-rate"); + + const CNFlow(this.value); + final String value; +} + +enum CNExchangeType { + direct("direct"), + reverse("reverse"); + + const CNExchangeType(this.value); + final String value; +} + class ChangeNowAPI { static const String scheme = "https"; static const String authority = "api.changenow.io"; - static const String apiVersion = "/v1"; static const String apiVersionV2 = "/v2"; final HTTP client; @@ -48,56 +59,24 @@ class ChangeNowAPI { static final ChangeNowAPI _instance = ChangeNowAPI(); static ChangeNowAPI get instance => _instance; - Uri _buildUri(String path, Map? params) { - return Uri.https(authority, apiVersion + path, params); - } - Uri _buildUriV2(String path, Map? params) { return Uri.https(authority, apiVersionV2 + path, params); } - Future _makeGetRequest(Uri uri) async { - try { - final response = await client.get( - url: uri, - headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, - ); - String? data; - try { - data = response.body; - final parsed = jsonDecode(data); - - return parsed; - } on FormatException catch (e) { - return { - "error": "Dart format exception", - "message": data, - }; - } - } catch (e, s) { - Logging.instance.e( - "_makeRequest($uri) threw", - error: e, - stackTrace: s, - ); - rethrow; - } - } - Future _makeGetRequestV2(Uri uri, String apiKey) async { + Logging.instance.t("ChangeNOW _makeGetRequestV2 to $uri"); + try { final response = await client.get( url: uri, headers: { - // 'Content-Type': 'application/json', - 'x-changenow-api-key': apiKey, + "Content-Type": "application/json", + "x-changenow-api-key": apiKey, }, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final data = response.body; @@ -105,27 +84,29 @@ class ChangeNowAPI { return parsed; } catch (e, s) { - Logging.instance.e( - "_makeRequestV2($uri) threw", - error: e, - stackTrace: s, - ); + Logging.instance.e("_makeRequestV2($uri) threw", error: e, stackTrace: s); rethrow; } } - Future _makePostRequest( + Future _makePostRequestV2( Uri uri, Map body, + String apiKey, ) async { + Logging.instance.t("ChangeNOW _makePostRequestV2 to $uri"); try { final response = await client.post( url: uri, - headers: {'Content-Type': 'application/json'}, + headers: { + "Content-Type": "application/json", + "x-changenow-api-key": apiKey, + }, body: jsonEncode(body), - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); String? data; @@ -144,7 +125,7 @@ class ChangeNowAPI { } } catch (e, s) { Logging.instance.e( - "_makePostRequest($uri) threw", + "_makePostRequestV2($uri) threw", error: e, stackTrace: s, ); @@ -152,129 +133,41 @@ class ChangeNowAPI { } } - /// This API endpoint returns the list of available currencies. + /// Retrieves the list of available currencies from the API. /// - /// Set [active] to true to return only active currencies. - /// Set [fixedRate] to true to return only currencies available on a fixed-rate flow. + /// - Set [active] to `true` to return only active currencies. + /// - Set [buy] to `true` to return only currencies available for buying. + /// - Set [sell] to `true` to return only currencies available for selling. + /// - Set [flow] to specify the type of exchange flow. + /// Options are [CNFlow.standard] (default) or [CNFlow.fixedRate]. Future>> getAvailableCurrencies({ - bool? fixedRate, bool? active, + bool? buy, + bool? sell, + CNFlow flow = CNFlow.standard, + String? apiKey, }) async { - Map? params; - - if (active != null || fixedRate != null) { - params = {}; - if (fixedRate != null) { - params.addAll({"fixedRate": fixedRate.toString()}); - } - if (active != null) { - params.addAll({"active": active.toString()}); - } - } - - final uri = _buildUri("/currencies", params); - - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); - - try { - final result = await compute( - _parseAvailableCurrenciesJson, - Tuple2(jsonArray as List, fixedRate == true), - ); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseAvailableCurrenciesJson( - Tuple2, bool> args, - ) { - try { - final List currencies = []; - - for (final json in args.item1) { - try { - final map = Map.from(json as Map); - currencies.add( - Currency.fromJson( - map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, - exchangeName: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - - return ExchangeResponse(value: currencies); - } catch (_) { - rethrow; - } - } - - Future>> getCurrenciesV2( - // { - // bool? fixedRate, - // bool? active, - // } - ) async { - Map? params; - - // if (active != null || fixedRate != null) { - // params = {}; - // if (fixedRate != null) { - // params.addAll({"fixedRate": fixedRate.toString()}); - // } - // if (active != null) { - // params.addAll({"active": active.toString()}); - // } - // } + final params = { + "flow": flow.value, + if (active != null) "active": active.toString(), + if (buy != null) "buy": buy.toString(), + if (sell != null) "sell": sell.toString(), + }; final uri = _buildUriV2("/exchange/currencies", params); try { // json array is expected here - final jsonArray = await _makeGetRequest(uri); + final jsonArray = await _makeGetRequestV2( + uri, + apiKey ?? kChangeNowApiKey, + ); try { - final result = await compute( - _parseV2CurrenciesJson, - jsonArray as List, - ); + final result = await compute(_parseAvailableCurrenciesJson, ( + jsonList: jsonArray as List, + fixedRateFlow: flow == CNFlow.fixedRate, + )); return result; } catch (e, s) { Logging.instance.e( @@ -304,21 +197,22 @@ class ChangeNowAPI { } } - ExchangeResponse> _parseV2CurrenciesJson( - List args, + ExchangeResponse> _parseAvailableCurrenciesJson( + ({List jsonList, bool fixedRateFlow}) args, ) { try { final List currencies = []; - for (final json in args) { + for (final json in args.jsonList) { try { final map = Map.from(json as Map); currencies.add( Currency.fromJson( map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, + rateType: + (map["supportsFixedRate"] as bool) + ? SupportedRateType.both + : SupportedRateType.estimated, exchangeName: ChangeNowExchange.exchangeName, ), ); @@ -338,110 +232,37 @@ class ChangeNowAPI { } } - /// This API endpoint returns the array of markets available for the specified currency be default. - /// The availability of a particular pair is determined by the 'isAvailable' field. + /// Retrieves the minimum amount required to exchange [fromCurrency] to [toCurrency]. /// - /// Required [ticker] to fetch paired currencies for. - /// Set [fixedRate] to true to return only currencies available on a fixed-rate flow. - Future>> getPairedCurrencies({ - required String ticker, - bool? fixedRate, - }) async { - Map? params; - - if (fixedRate != null) { - params = {}; - params.addAll({"fixedRate": fixedRate.toString()}); - } - - final uri = _buildUri("/currencies-to/$ticker", params); - - try { - // json array is expected here - - final response = await _makeGetRequest(uri); - - if (response is Map && response["error"] != null) { - return ExchangeResponse( - exception: UnsupportedCurrencyException( - response["message"] as String? ?? response["error"].toString(), - ExchangeExceptionType.generic, - ticker, - ), - ); - } - - final jsonArray = response as List; - - final List currencies = []; - try { - for (final json in jsonArray) { - try { - final map = Map.from(json as Map); - currencies.add( - Currency.fromJson( - map, - rateType: (map["supportsFixedRate"] as bool) - ? SupportedRateType.both - : SupportedRateType.estimated, - exchangeName: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - } catch (e, s) { - Logging.instance.e( - "getPairedCurrencies exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - return ExchangeResponse(value: currencies); - } catch (e, s) { - Logging.instance.e( - "getPairedCurrencies exception", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// The API endpoint returns minimal payment amount required to make - /// an exchange of [fromTicker] to [toTicker]. - /// If you try to exchange less, the transaction will most likely fail. + /// If you attempt to exchange less than this amount, the transaction may fail. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "usdt"). + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Exchange flow type. Defaults to [CNFlow.standard]. + /// - [apiKey]: (Optional) API key if required. Future> getMinimalExchangeAmount({ - required String fromTicker, - required String toTicker, + required String fromCurrency, + required String toCurrency, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, String? apiKey, }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "flow": flow.value, + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, }; - final uri = _buildUri("/min-amount/${fromTicker}_$toTicker", params); + final uri = _buildUriV2("/exchange/min-amount", params); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { final value = Decimal.parse(json["minAmount"].toString()); @@ -469,27 +290,40 @@ class ChangeNowAPI { } } - /// The API endpoint returns minimal payment amount and maximum payment amount - /// required to make an exchange. If you try to exchange less than minimum or - /// more than maximum, the transaction will most likely fail. Any pair of - /// assets has minimum amount and some of pairs have maximum amount. + /// Retrieves the minimum and maximum exchangeable amounts for a given currency pair. + /// + /// Attempting to exchange less than the minimum or more than the maximum may result in a failed transaction. + /// Every asset pair has a minimum exchange amount, and some also have a maximum. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "eth"). + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Type of exchange flow. Defaults to [CNFlow.standard]. + /// - [apiKey]: (Optional) API key if required. Future> getRange({ - required String fromTicker, - required String toTicker, - required bool isFixedRate, + required String fromCurrency, + required String toCurrency, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, String? apiKey, }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + "flow": flow.value, + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, }; - final uri = _buildUri( - "/exchange-range${isFixedRate ? "/fixed-rate" : ""}/${fromTicker}_$toTicker", - params, - ); + final uri = _buildUriV2("/exchange/range", params); try { - final jsonObject = await _makeGetRequest(uri); + final jsonObject = await _makeGetRequestV2( + uri, + apiKey ?? kChangeNowApiKey, + ); final json = Map.from(jsonObject as Map); return ExchangeResponse( @@ -499,11 +333,7 @@ class ChangeNowAPI { ), ); } catch (e, s) { - Logging.instance.e( - "getRange exception: ", - error: e, - stackTrace: s, - ); + Logging.instance.f("getRange exception: ", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -513,24 +343,50 @@ class ChangeNowAPI { } } - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] + /// Retrieves an estimated amount of [toCurrency] you would receive for a given input amount of [fromCurrency]. + /// + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "eth"). + /// - [fromAmount]: (Required if [type] is [CNExchangeType.direct]) Amount to exchange. Must be greater than 0. + /// - [toAmount]: (Required if [type] is [CNExchangeType.reverse]) Desired amount to receive. Must be greater than 0. + /// - [fromNetwork]: (Optional) Network of the currency you want to exchange (e.g., "btc"). + /// - [toNetwork]: (Optional) Network of the currency you want to receive (e.g., "eth"). + /// - [flow]: (Optional) Type of exchange flow. Defaults to [CNFlow.standard]. + /// - [type]: (Optional) Exchange direction. Either [CNExchangeType.direct] or [CNExchangeType.reverse]. Defaults to [CNExchangeType.direct]. + /// - [useRateId]: (Optional) For fixed-rate flow. When true, the response includes a [rateId] for locking the rate in the next request. + /// - [isTopUp]: (Optional) If true, gets an estimate for a balance top-up (no withdrawal fee). + /// - [apiKey]: (Optional) API key if required. Future> getEstimatedExchangeAmount({ - required String fromTicker, - required String toTicker, - required Decimal fromAmount, + required String fromCurrency, + required String toCurrency, + Decimal? fromAmount, + Decimal? toAmount, + String? fromNetwork, + String? toNetwork, + CNFlow flow = CNFlow.standard, + CNExchangeType type = CNExchangeType.direct, + bool? useRateId, + bool? isTopUp, String? apiKey, }) async { - final Map params = {"api_key": apiKey ?? kChangeNowApiKey}; + final params = { + "fromCurrency": fromCurrency, + "toCurrency": toCurrency, + if (fromAmount != null) "fromAmount": fromAmount.toString(), + if (toAmount != null) "toAmount": toAmount.toString(), + if (fromNetwork != null) "fromNetwork": fromNetwork, + if (toNetwork != null) "toNetwork": toNetwork, + "flow": flow.value, + "type": type.value, + if (useRateId != null) "useRateId": useRateId.toString(), + if (isTopUp != null) "isTopUp": isTopUp.toString(), + }; - final uri = _buildUri( - "/exchange-amount/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); + final uri = _buildUriV2("/exchange/estimated-amount", params); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { final map = Map.from(json as Map); @@ -554,104 +410,19 @@ class ChangeNowAPI { } final value = EstimatedExchangeAmount.fromJson(map); + final reversed = value.type == CNExchangeType.reverse; return ExchangeResponse( value: Estimate( - estimatedAmount: value.estimatedAmount, - fixedRate: false, - reversed: false, - rateId: value.rateId, - warningMessage: value.warningMessage, - exchangeProvider: ChangeNowExchange.exchangeName, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getEstimatedExchangeAmount exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] - Future> getEstimatedExchangeAmountFixedRate({ - required String fromTicker, - required String toTicker, - required Decimal fromAmount, - required bool reversed, - bool useRateId = true, - String? apiKey, - }) async { - final Map params = { - "api_key": apiKey ?? kChangeNowApiKey, - "useRateId": useRateId.toString(), - }; - - late final Uri uri; - if (reversed) { - uri = _buildUri( - "/exchange-deposit/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); - } else { - uri = _buildUri( - "/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - params, - ); - } - - try { - // simple json object is expected here - final json = await _makeGetRequest(uri); - - try { - final map = Map.from(json as Map); - - if (map["error"] != null) { - if (map["error"] == "not_valid_fixed_rate_pair") { - return ExchangeResponse( - exception: PairUnavailableException( - map["message"] as String? ?? "Unsupported fixed rate pair", - ExchangeExceptionType.generic, - ), - ); - } else { - return ExchangeResponse( - exception: ExchangeException( - map["message"] as String? ?? map["error"].toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - final value = EstimatedExchangeAmount.fromJson(map); - return ExchangeResponse( - value: Estimate( - estimatedAmount: value.estimatedAmount, - fixedRate: true, + estimatedAmount: reversed ? value.fromAmount : value.toAmount, + fixedRate: value.flow == CNFlow.fixedRate, reversed: reversed, rateId: value.rateId, warningMessage: value.warningMessage, exchangeProvider: ChangeNowExchange.exchangeName, ), ); - } catch (_) { + } catch (e, s) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -674,325 +445,100 @@ class ChangeNowAPI { } } - // old v1 version - /// This API endpoint returns fixed-rate estimated exchange amount of - /// [toTicker] cryptocurrency to receive for [fromAmount] of [fromTicker] - // Future> - // getEstimatedFixedRateExchangeAmount({ - // required String fromTicker, - // required String toTicker, - // required Decimal fromAmount, - // // (Optional) Use rateId for fixed-rate flow. If this field is true, you - // // could use returned field "rateId" in next method for creating transaction - // // to freeze estimated amount that you got in this method. Current estimated - // // amount would be valid until time in field "validUntil" - // bool useRateId = true, - // String? apiKey, - // }) async { - // Map params = { - // "api_key": apiKey ?? kChangeNowApiKey, - // "useRateId": useRateId.toString(), - // }; - // - // final uri = _buildUri( - // "/exchange-amount/fixed-rate/${fromAmount.toString()}/${fromTicker}_$toTicker", - // params, - // ); - // - // try { - // // simple json object is expected here - // final json = await _makeGetRequest(uri); - // - // try { - // final value = EstimatedExchangeAmount.fromJson( - // Map.from(json as Map)); - // return ExchangeResponse(value: value); - // } catch (_) { - // return ExchangeResponse( - // exception: ExchangeException( - // "Failed to serialize $json", - // ExchangeExceptionType.serializeResponseError, - // ), - // ); - // } - // } catch (e, s) { - // Logging.instance.log( - // "getEstimatedFixedRateExchangeAmount exception: $e\n$s", - // level: LogLevel.Error); - // return ExchangeResponse( - // exception: ExchangeException( - // e.toString(), - // ExchangeExceptionType.generic, - // ), - // ); - // } - // } - - /// Get estimated amount of [toTicker] cryptocurrency to receive - /// for [fromAmount] of [fromTicker] - Future> getEstimatedExchangeAmountV2({ - required String fromTicker, - required String toTicker, - required CNEstimateType fromOrTo, - required Decimal amount, - String? fromNetwork, - String? toNetwork, - CNFlowType flow = CNFlowType.standard, + /// Creates a new exchange transaction. + /// + /// This method initializes a currency exchange by specifying the source and destination + /// currencies, the exchange direction, recipient details, and optional metadata. + /// + /// If using a fixed-rate flow, you **must** provide a [rateId] obtained from a prior estimate + /// to lock in the rate. If using a standard flow, [rateId] can be left null. + /// + /// Parameters: + /// - [fromCurrency]: Ticker of the currency you want to exchange (e.g., "btc"). + /// - [fromNetwork]: Network of the currency you want to exchange (e.g., "btc"). + /// - [toCurrency]: Ticker of the currency you want to receive (e.g., "usdt"). + /// - [toNetwork]: Network of the currency you want to receive (e.g., "eth"). + /// - [fromAmount]: Amount of currency you want to exchange (used in "direct" flow). + /// - [toAmount]: Amount of currency you want to receive (used in "reverse" flow). + /// - [flow]: Type of exchange flow. Either [CNFlow.standard] or [CNFlow.fixedRate]. + /// - [type]: Direction of the exchange. Use [CNExchangeType.direct] to define the amount to send, + /// or [CNExchangeType.reverse] to define the amount to receive. + /// - [address]: Wallet address that will receive the exchanged funds. + /// - [extraId]: (Optional) Extra ID required by some currencies (e.g., memo, tag). + /// - [refundAddress]: (Optional) Address used to refund funds in case of timeout or failure. + /// - [refundExtraId]: (Optional) Extra ID for the refund address if required. + /// - [userId]: (Optional) Internal user identifier for partners with special access. + /// - [payload]: (Optional) Arbitrary string to store additional context for the transaction. + /// - [contactEmail]: (Optional) Email address to contact the user in case of issues. + /// - [rateId]: (Required for fixed-rate) The rate ID returned from the estimate step to freeze the exchange rate. + /// - [apiKey]: (Optional) Your API key, if authentication is required. + /// + /// Returns a [Future] resolving to [ExchangeResponse] containing the created [ExchangeTransaction]. + Future> createExchangeTransaction({ + required String fromCurrency, + required String fromNetwork, + required String toCurrency, + required String toNetwork, + Decimal? fromAmount, + Decimal? toAmount, + CNFlow flow = CNFlow.standard, + CNExchangeType type = CNExchangeType.direct, + required String address, + String? extraId, + String? refundAddress, + String? refundExtraId, + String? userId, + String? payload, + String? contactEmail, + required String? rateId, String? apiKey, }) async { - final Map params = { - "fromCurrency": fromTicker, - "toCurrency": toTicker, + final Map body = { + "fromCurrency": fromCurrency, + "fromNetwork": fromNetwork ?? "", + "toCurrency": toCurrency, + "toNetwork": toNetwork ?? "", + "fromAmount": fromAmount?.toString() ?? "", + "toAmount": toAmount?.toString() ?? "", "flow": flow.value, - "type": fromOrTo.name, + "type": type.value, + "address": address, + "extraId": extraId ?? "", + "refundAddress": refundAddress ?? "", + "refundExtraId": refundExtraId ?? "", + "userId": userId ?? "", + "payload": payload ?? "", + "contactEmail": contactEmail ?? "", + "rateId": rateId ?? "", }; - switch (fromOrTo) { - case CNEstimateType.direct: - params["fromAmount"] = amount.toString(); - break; - case CNEstimateType.reverse: - params["toAmount"] = amount.toString(); - break; - } - - if (fromNetwork != null) { - params["fromNetwork"] = fromNetwork; - } - - if (toNetwork != null) { - params["toNetwork"] = toNetwork; - } - - if (flow == CNFlowType.fixedRate) { - params["useRateId"] = "true"; - } - - final uri = _buildUriV2("/exchange/estimated-amount", params); + final uri = _buildUriV2("/exchange", null); try { // simple json object is expected here - final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); - - try { - final value = - CNExchangeEstimate.fromJson(Map.from(json as Map)); - return ExchangeResponse(value: value); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getEstimatedExchangeAmountV2 exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), + final json = await _makePostRequestV2( + uri, + body, + apiKey ?? kChangeNowApiKey, ); - } - } - - /// This API endpoint returns the list of all the pairs available on a - /// fixed-rate flow. Some currencies get enabled or disabled from time to - /// time and the market info gets updates, so make sure to refresh the list - /// occasionally. One time per minute is sufficient. - Future>> getAvailableFixedRateMarkets({ - String? apiKey, - }) async { - final uri = _buildUri( - "/market-info/fixed-rate/${apiKey ?? kChangeNowApiKey}", - null, - ); - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); + json["date"] = DateTime.now().toIso8601String(); try { - final result = - await compute(_parseFixedRateMarketsJson, jsonArray as List); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableFixedRateMarkets exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), + final value = CNExchangeTransaction.fromJson( + Map.from(json as Map), ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableFixedRateMarkets exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseFixedRateMarketsJson( - List jsonArray, - ) { - try { - final List markets = []; - for (final json in jsonArray) { - try { - markets.add( - FixedRateMarket.fromMap(Map.from(json as Map)), - ); - } catch (_) { + return ExchangeResponse(value: value); + } catch (e, s) { + if (json["error"] == "rate_id_not_found_or_expired") { return ExchangeResponse( exception: ExchangeException( - "Failed to serialize $json", + "Rate ID not found or expired", ExchangeExceptionType.serializeResponseError, ), ); } - } - return ExchangeResponse(value: markets); - } catch (_) { - rethrow; - } - } - - /// The API endpoint creates a transaction, generates an address for - /// sending funds and returns transaction attributes. - Future> - createStandardExchangeTransaction({ - required String fromTicker, - required String toTicker, - required String receivingAddress, - required Decimal amount, - String extraId = "", - String userId = "", - String contactEmail = "", - String refundAddress = "", - String refundExtraId = "", - String? apiKey, - }) async { - final Map map = { - "from": fromTicker, - "to": toTicker, - "address": receivingAddress, - "amount": amount.toString(), - "flow": "standard", - "extraId": extraId, - "userId": userId, - "contactEmail": contactEmail, - "refundAddress": refundAddress, - "refundExtraId": refundExtraId, - }; - - final uri = _buildUri("/transactions/${apiKey ?? kChangeNowApiKey}", null); - - try { - // simple json object is expected here - final json = await _makePostRequest(uri, map); - - // pass in date to prevent using default 1970 date - json["date"] = DateTime.now().toString(); - - try { - final value = ExchangeTransaction.fromJson( - Map.from(json as Map), - ); - return ExchangeResponse(value: value); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "createStandardExchangeTransaction exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - /// The API endpoint creates a transaction, generates an address for - /// sending funds and returns transaction attributes. - Future> - createFixedRateExchangeTransaction({ - required String fromTicker, - required String toTicker, - required String receivingAddress, - required Decimal amount, - required String rateId, - required bool reversed, - String extraId = "", - String userId = "", - String contactEmail = "", - String refundAddress = "", - String refundExtraId = "", - String? apiKey, - }) async { - final Map map = { - "from": fromTicker, - "to": toTicker, - "address": receivingAddress, - "flow": "fixed-rate", - "extraId": extraId, - "userId": userId, - "contactEmail": contactEmail, - "refundAddress": refundAddress, - "refundExtraId": refundExtraId, - "rateId": rateId, - }; - - if (reversed) { - map["result"] = amount.toString(); - } else { - map["amount"] = amount.toString(); - } - - final uri = _buildUri( - "/transactions/fixed-rate${reversed ? "/from-result" : ""}/${apiKey ?? kChangeNowApiKey}", - null, - ); - - try { - // simple json object is expected here - final json = await _makePostRequest(uri, map); - - // pass in date to prevent using default 1970 date - json["date"] = DateTime.now().toString(); - - try { - final value = ExchangeTransaction.fromJson( - Map.from(json as Map), - ); - return ExchangeResponse(value: value); - } catch (_) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -1015,23 +561,23 @@ class ChangeNowAPI { } } - Future> getTransactionStatus({ + Future> getTransactionStatus({ required String id, String? apiKey, }) async { - final uri = - _buildUri("/transactions/$id/${apiKey ?? kChangeNowApiKey}", null); + final uri = _buildUriV2("/exchange/by-id", {"id": id}); try { // simple json object is expected here - final json = await _makeGetRequest(uri); + final json = await _makeGetRequestV2(uri, apiKey ?? kChangeNowApiKey); try { - final value = ExchangeTransactionStatus.fromJson( + final value = CNExchangeTransactionStatus.fromMap( Map.from(json as Map), ); return ExchangeResponse(value: value); - } catch (_) { + } catch (e, s) { + Logging.instance.f(json, error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( "Failed to serialize $json", @@ -1053,81 +599,4 @@ class ChangeNowAPI { ); } } - - Future>> getAvailableFloatingRatePairs({ - bool includePartners = false, - }) async { - final uri = _buildUri( - "/market-info/available-pairs", - {"includePartners": includePartners.toString()}, - ); - - try { - // json array is expected here - final jsonArray = await _makeGetRequest(uri); - - try { - final result = await compute( - _parseAvailableFloatingRatePairsJson, - jsonArray as List, - ); - return result; - } catch (e, s) { - Logging.instance.e( - "getAvailableFloatingRatePairs exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - "Error: $jsonArray", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } catch (e, s) { - Logging.instance.e( - "getAvailableFloatingRatePairs exception: ", - error: e, - stackTrace: s, - ); - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - ExchangeResponse> _parseAvailableFloatingRatePairsJson( - List jsonArray, - ) { - try { - final List pairs = []; - for (final json in jsonArray) { - try { - final List stringPair = (json as String).split("_"); - pairs.add( - Pair( - exchangeName: ChangeNowExchange.exchangeName, - from: stringPair[0], - to: stringPair[1], - rateType: SupportedRateType.estimated, - ), - ); - } catch (_) { - return ExchangeResponse( - exception: ExchangeException( - "Failed to serialize $json", - ExchangeExceptionType.serializeResponseError, - ), - ); - } - } - return ExchangeResponse(value: pairs); - } catch (_) { - rethrow; - } - } } diff --git a/lib/services/exchange/change_now/change_now_exchange.dart b/lib/services/exchange/change_now/change_now_exchange.dart index 35e8e30b2..48389afeb 100644 --- a/lib/services/exchange/change_now/change_now_exchange.dart +++ b/lib/services/exchange/change_now/change_now_exchange.dart @@ -9,16 +9,16 @@ */ import 'package:decimal/decimal.dart'; -import '../../../models/exchange/change_now/exchange_transaction.dart'; +import 'package:uuid/uuid.dart'; + import '../../../models/exchange/response_objects/estimate.dart'; import '../../../models/exchange/response_objects/range.dart'; import '../../../models/exchange/response_objects/trade.dart'; import '../../../models/isar/exchange_cache/currency.dart'; import '../../../models/isar/exchange_cache/pair.dart'; -import 'change_now_api.dart'; import '../exchange.dart'; import '../exchange_response.dart'; -import 'package:uuid/uuid.dart'; +import 'change_now_api.dart'; class ChangeNowExchange extends Exchange { ChangeNowExchange._(); @@ -35,6 +35,8 @@ class ChangeNowExchange extends Exchange { Future> createTrade({ required String from, required String to, + String? fromNetwork, + String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -44,45 +46,35 @@ class ChangeNowExchange extends Exchange { Estimate? estimate, required bool reversed, }) async { - late final ExchangeResponse response; - if (fixedRate) { - response = await ChangeNowAPI.instance.createFixedRateExchangeTransaction( - fromTicker: from, - toTicker: to, - receivingAddress: addressTo, - amount: amount, - rateId: estimate!.rateId!, - extraId: extraId ?? "", - refundAddress: addressRefund, - refundExtraId: refundExtraId, - reversed: reversed, - ); - } else { - response = await ChangeNowAPI.instance.createStandardExchangeTransaction( - fromTicker: from, - toTicker: to, - receivingAddress: addressTo, - amount: amount, - extraId: extraId ?? "", - refundAddress: addressRefund, - refundExtraId: refundExtraId, - ); - } + final response = await ChangeNowAPI.instance.createExchangeTransaction( + fromCurrency: from, + fromNetwork: fromNetwork ?? "", + toCurrency: to, + toNetwork: toNetwork ?? "", + address: addressTo, + rateId: estimate?.rateId, + refundAddress: addressRefund, + refundExtraId: refundExtraId, + fromAmount: reversed ? null : amount, + toAmount: reversed ? amount : null, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, + type: reversed ? CNExchangeType.reverse : CNExchangeType.direct, + ); + if (response.exception != null) { return ExchangeResponse(exception: response.exception); } - final statusResponse = await ChangeNowAPI.instance - .getTransactionStatus(id: response.value!.id); + final statusResponse = await ChangeNowAPI.instance.getTransactionStatus( + id: response.value!.id, + ); if (statusResponse.exception != null) { return ExchangeResponse(exception: statusResponse.exception); } return ExchangeResponse( - value: Trade.fromExchangeTransaction( - response.value!.copyWith( - statusObject: statusResponse.value!, - ), + value: Trade.fromCNExchangeTransaction( + response.value!.copyWithStatus(statusResponse.value!), reversed, ), ); @@ -92,75 +84,70 @@ class ChangeNowExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - return await ChangeNowAPI.instance.getCurrenciesV2(); - // return await ChangeNowAPI.instance.getAvailableCurrencies( - // fixedRate: fixedRate ? true : null, - // active: true, - // ); - } - - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - return await ChangeNowAPI.instance.getPairedCurrencies( - ticker: forCurrency, - fixedRate: fixedRate, - ); - } - - @override - Future>> getAllPairs(bool fixedRate) async { - if (fixedRate) { - final markets = - await ChangeNowAPI.instance.getAvailableFixedRateMarkets(); - - if (markets.value == null) { - return ExchangeResponse(exception: markets.exception); - } - - final List pairs = []; - for (final market in markets.value!) { - pairs.add( - Pair( - exchangeName: ChangeNowExchange.exchangeName, - from: market.from, - to: market.to, - rateType: SupportedRateType.fixed, - ), - ); - } - return ExchangeResponse(value: pairs); - } else { - return await ChangeNowAPI.instance.getAvailableFloatingRatePairs(); - } + return await ChangeNowAPI.instance.getAvailableCurrencies(); } + // + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // return await ChangeNowAPI.instance.getPairedCurrencies( + // ticker: forCurrency, + // fixedRate: fixedRate, + // ); + // } + // + // @override + // Future>> getAllPairs(bool fixedRate) async { + // if (fixedRate) { + // final markets = + // await ChangeNowAPI.instance.getAvailableFixedRateMarkets(); + // + // if (markets.value == null) { + // return ExchangeResponse(exception: markets.exception); + // } + // + // final List pairs = []; + // for (final market in markets.value!) { + // pairs.add( + // Pair( + // exchangeName: ChangeNowExchange.exchangeName, + // from: market.from, + // to: market.to, + // rateType: SupportedRateType.fixed, + // ), + // ); + // } + // return ExchangeResponse(value: pairs); + // } else { + // return await ChangeNowAPI.instance.getAvailableFloatingRatePairs(); + // } + // } @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, ) async { - late final ExchangeResponse response; - if (fixedRate) { - response = - await ChangeNowAPI.instance.getEstimatedExchangeAmountFixedRate( - fromTicker: from, - toTicker: to, - fromAmount: amount, - reversed: reversed, - ); - } else { - response = await ChangeNowAPI.instance.getEstimatedExchangeAmount( - fromTicker: from, - toTicker: to, - fromAmount: amount, - ); - } + final response = await ChangeNowAPI.instance.getEstimatedExchangeAmount( + fromCurrency: from, + fromNetwork: fromNetwork, + toCurrency: to, + toNetwork: toNetwork, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, + type: reversed ? CNExchangeType.reverse : CNExchangeType.direct, + fromAmount: reversed ? null : amount, + toAmount: reversed ? amount : null, + + useRateId: fixedRate ? true : null, + ); + return ExchangeResponse( value: response.value == null ? null : [response.value!], exception: response.exception, @@ -170,13 +157,17 @@ class ChangeNowExchange extends Exchange { @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { return await ChangeNowAPI.instance.getRange( - fromTicker: from, - toTicker: to, - isFixedRate: fixedRate, + fromCurrency: from, + fromNetwork: fromNetwork, + toCurrency: to, + toNetwork: toNetwork, + flow: fixedRate ? CNFlow.fixedRate : CNFlow.standard, ); } @@ -191,8 +182,9 @@ class ChangeNowExchange extends Exchange { @override Future> getTrade(String tradeId) async { - final response = - await ChangeNowAPI.instance.getTransactionStatus(id: tradeId); + final response = await ChangeNowAPI.instance.getTransactionStatus( + id: tradeId, + ); if (response.exception != null) { return ExchangeResponse(exception: response.exception); } @@ -207,19 +199,19 @@ class ChangeNowExchange extends Exchange { timestamp: timestamp, updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp, payInCurrency: t.fromCurrency, - payInAmount: t.expectedSendAmountDecimal, + payInAmount: t.expectedAmountFrom ?? "", payInAddress: t.payinAddress, payInNetwork: "", - payInExtraId: t.payinExtraId, - payInTxid: t.payinHash, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", payOutCurrency: t.toCurrency, - payOutAmount: t.expectedReceiveAmountDecimal, + payOutAmount: t.expectedAmountTo ?? "", payOutAddress: t.payoutAddress, payOutNetwork: "", - payOutExtraId: t.payoutExtraId, - payOutTxid: t.payoutHash, - refundAddress: t.refundAddress, - refundExtraId: t.refundExtraId, + payOutExtraId: t.payoutExtraId ?? "", + payOutTxid: t.payoutHash ?? "", + refundAddress: t.refundAddress ?? "", + refundExtraId: t.refundExtraId ?? "", status: t.status.name, exchangeName: ChangeNowExchange.exchangeName, ); @@ -229,8 +221,9 @@ class ChangeNowExchange extends Exchange { @override Future> updateTrade(Trade trade) async { - final response = - await ChangeNowAPI.instance.getTransactionStatus(id: trade.tradeId); + final response = await ChangeNowAPI.instance.getTransactionStatus( + id: trade.tradeId, + ); if (response.exception != null) { return ExchangeResponse(exception: response.exception); } @@ -245,23 +238,19 @@ class ChangeNowExchange extends Exchange { timestamp: timestamp, updatedAt: DateTime.tryParse(t.updatedAt) ?? timestamp, payInCurrency: t.fromCurrency, - payInAmount: t.amountSendDecimal.isEmpty - ? t.expectedSendAmountDecimal - : t.amountSendDecimal, + payInAmount: t.amountFrom ?? t.expectedAmountFrom ?? trade.payInAmount, payInAddress: t.payinAddress, payInNetwork: trade.payInNetwork, - payInExtraId: t.payinExtraId, - payInTxid: t.payinHash, + payInExtraId: t.payinExtraId ?? "", + payInTxid: t.payinHash ?? "", payOutCurrency: t.toCurrency, - payOutAmount: t.amountReceiveDecimal.isEmpty - ? t.expectedReceiveAmountDecimal - : t.amountReceiveDecimal, + payOutAmount: t.amountTo ?? t.expectedAmountTo ?? trade.payOutAmount, payOutAddress: t.payoutAddress, payOutNetwork: trade.payOutNetwork, - payOutExtraId: t.payoutExtraId, - payOutTxid: t.payoutHash, - refundAddress: t.refundAddress, - refundExtraId: t.refundExtraId, + payOutExtraId: t.payoutExtraId ?? "", + payOutTxid: t.payoutHash ?? "", + refundAddress: t.refundAddress ?? "", + refundExtraId: t.refundExtraId ?? "", status: t.status.name, exchangeName: ChangeNowExchange.exchangeName, ); diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart index bf592fab3..f25bddc77 100644 --- a/lib/services/exchange/exchange.dart +++ b/lib/services/exchange/exchange.dart @@ -14,7 +14,6 @@ import '../../models/exchange/response_objects/estimate.dart'; import '../../models/exchange/response_objects/range.dart'; import '../../models/exchange/response_objects/trade.dart'; import '../../models/isar/exchange_cache/currency.dart'; -import '../../models/isar/exchange_cache/pair.dart'; import 'change_now/change_now_exchange.dart'; import 'exchange_response.dart'; import 'majestic_bank/majestic_bank_exchange.dart'; @@ -53,17 +52,17 @@ abstract class Exchange { Future>> getAllCurrencies(bool fixedRate); - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ); - - Future>> getPairsFor( - String currency, - bool fixedRate, - ); + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ); - Future>> getAllPairs(bool fixedRate); + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ); + // + // Future>> getAllPairs(bool fixedRate); Future> getTrade(String tradeId); Future> updateTrade(Trade trade); @@ -72,13 +71,17 @@ abstract class Exchange { Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ); Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -87,6 +90,8 @@ abstract class Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -101,10 +106,10 @@ abstract class Exchange { /// /// Add to this list when adding a new exchange which supports Tor. static List get exchangesWithTorSupport => [ - MajesticBankExchange.instance, - TrocadorExchange.instance, - NanswapExchange.instance, // Maybe?? - ]; + MajesticBankExchange.instance, + TrocadorExchange.instance, + NanswapExchange.instance, // Maybe?? + ]; /// List of exchange names which support Tor. /// diff --git a/lib/services/exchange/majestic_bank/majestic_bank_api.dart b/lib/services/exchange/majestic_bank/majestic_bank_api.dart index 210be8af4..ee87bf159 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_api.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_api.dart @@ -50,9 +50,10 @@ class MajesticBankAPI { try { final response = await client.get( url: uri, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); code = response.code; @@ -61,16 +62,17 @@ class MajesticBankAPI { return parsed; } catch (e, s) { - Logging.instance - .e("_makeRequest($uri) HTTP:$code threw: ", error: e, stackTrace: s); + Logging.instance.e( + "_makeRequest($uri) HTTP:$code threw: ", + error: e, + stackTrace: s, + ); rethrow; } } Future>> getRates() async { - final uri = _buildUri( - endpoint: "rates", - ); + final uri = _buildUri(endpoint: "rates"); try { final jsonObject = await _makeGetRequest(uri); @@ -90,11 +92,7 @@ class MajesticBankAPI { } return ExchangeResponse(value: rates); } catch (e, s) { - Logging.instance.e( - "getRates exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getRates exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -109,9 +107,7 @@ class MajesticBankAPI { }) async { final uri = _buildUri( endpoint: "limits", - params: { - "from_currency": fromCurrency, - }, + params: {"from_currency": fromCurrency}, ); try { @@ -127,11 +123,7 @@ class MajesticBankAPI { return ExchangeResponse(value: limit); } catch (e, s) { - Logging.instance.e( - "getLimits exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getLimits exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -163,11 +155,7 @@ class MajesticBankAPI { return ExchangeResponse(value: limits); } catch (e, s) { - Logging.instance.e( - "getLimits exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("getLimits exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -196,10 +184,7 @@ class MajesticBankAPI { params["from_amount"] = amount; } - final uri = _buildUri( - endpoint: "calculate", - params: params, - ); + final uri = _buildUri(endpoint: "calculate", params: params); try { final jsonObject = await _makeGetRequest(uri); @@ -284,11 +269,7 @@ class MajesticBankAPI { return ExchangeResponse(value: order); } catch (e, s) { - Logging.instance.e( - "createOrder exception", - error: e, - stackTrace: s, - ); + Logging.instance.e("createOrder exception", error: e, stackTrace: s); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -342,8 +323,11 @@ class MajesticBankAPI { return ExchangeResponse(value: order); } catch (e, s) { - Logging.instance - .e("createFixedRateOrder exception: ", error: e, stackTrace: s); + Logging.instance.e( + "createFixedRateOrder exception: ", + error: e, + stackTrace: s, + ); return ExchangeResponse( exception: ExchangeException( e.toString(), @@ -356,12 +340,7 @@ class MajesticBankAPI { Future> trackOrder({ required String orderId, }) async { - final uri = _buildUri( - endpoint: "track", - params: { - "trx": orderId, - }, - ); + final uri = _buildUri(endpoint: "track", params: {"trx": orderId}); try { final jsonObject = await _makeGetRequest(uri); diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index 2d283e69e..b1f8be150 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -53,6 +53,8 @@ class MajesticBankExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -138,7 +140,8 @@ class MajesticBankExchange extends Exchange { final currency = Currency( exchangeName: MajesticBankExchange.exchangeName, ticker: limit.currency, - name: kMajesticBankCurrencyNames[limit.currency] ?? + name: + kMajesticBankCurrencyNames[limit.currency] ?? limit.currency, // todo: add more names if MB adds more network: "", image: "", @@ -154,42 +157,44 @@ class MajesticBankExchange extends Exchange { return ExchangeResponse(value: currencies); } - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) { - // TODO: change this if the api changes to allow getting by paired currency - return getAllCurrencies(fixedRate); - } - - @override - Future>> getAllPairs(bool fixedRate) async { - final response = await MajesticBankAPI.instance.getRates(); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final List pairs = []; - final rates = response.value!; - - for (final rate in rates) { - final pair = Pair( - exchangeName: MajesticBankExchange.exchangeName, - from: rate.fromCurrency, - to: rate.toCurrency, - rateType: SupportedRateType.both, - ); - pairs.add(pair); - } - - return ExchangeResponse(value: pairs); - } + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) { + // // TODO: change this if the api changes to allow getting by paired currency + // return getAllCurrencies(fixedRate); + // } + // + // @override + // Future>> getAllPairs(bool fixedRate) async { + // final response = await MajesticBankAPI.instance.getRates(); + // if (response.value == null) { + // return ExchangeResponse(exception: response.exception); + // } + // + // final List pairs = []; + // final rates = response.value!; + // + // for (final rate in rates) { + // final pair = Pair( + // exchangeName: MajesticBankExchange.exchangeName, + // from: rate.fromCurrency, + // to: rate.toCurrency, + // rateType: SupportedRateType.both, + // ); + // pairs.add(pair); + // } + // + // return ExchangeResponse(value: pairs); + // } @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -214,33 +219,36 @@ class MajesticBankExchange extends Exchange { return ExchangeResponse(value: [estimate]); } - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - final response = await getAllPairs(fixedRate); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final pairs = response.value!.where( - (e) => - e.from.toUpperCase() == currency.toUpperCase() || - e.to.toUpperCase() == currency.toUpperCase(), - ); - - return ExchangeResponse(value: pairs.toList()); - } + // @override + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ) async { + // final response = await getAllPairs(fixedRate); + // if (response.value == null) { + // return ExchangeResponse(exception: response.exception); + // } + // + // final pairs = response.value!.where( + // (e) => + // e.from.toUpperCase() == currency.toUpperCase() || + // e.to.toUpperCase() == currency.toUpperCase(), + // ); + // + // return ExchangeResponse(value: pairs.toList()); + // } @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { - final response = - await MajesticBankAPI.instance.getLimit(fromCurrency: from); + final response = await MajesticBankAPI.instance.getLimit( + fromCurrency: from, + ); if (response.value == null) { return ExchangeResponse(exception: response.exception); } diff --git a/lib/services/exchange/nanswap/nanswap_exchange.dart b/lib/services/exchange/nanswap/nanswap_exchange.dart index de89b886e..2392199e7 100644 --- a/lib/services/exchange/nanswap/nanswap_exchange.dart +++ b/lib/services/exchange/nanswap/nanswap_exchange.dart @@ -30,6 +30,8 @@ class NanswapExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -74,9 +76,7 @@ class NanswapExchange extends Exchange { ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -108,9 +108,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -136,34 +134,31 @@ class NanswapExchange extends Exchange { final response = await NanswapAPI.instance.getSupportedCurrencies(); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } return ExchangeResponse( - value: response.value! - .where((e) => filter.contains(e.id)) - .map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.id, - name: e.name, - network: e.network, - image: e.image, - isFiat: false, - rateType: SupportedRateType.estimated, - isStackCoin: AppConfig.isStackCoin(e.id), - tokenContract: null, - isAvailable: true, - ), - ) - .toList(), + value: + response.value! + .where((e) => filter.contains(e.id)) + .map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.id, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.estimated, + isStackCoin: AppConfig.isStackCoin(e.id), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -174,15 +169,12 @@ class NanswapExchange extends Exchange { } } - @override - Future>> getAllPairs(bool fixedRate) async { - throw UnimplementedError(); - } - @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -211,9 +203,7 @@ class NanswapExchange extends Exchange { } if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -231,9 +221,7 @@ class NanswapExchange extends Exchange { ], ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -243,59 +231,53 @@ class NanswapExchange extends Exchange { ); } } - - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - try { - if (fixedRate) { - throw ExchangeException( - "Nanswap fixedRate not available", - ExchangeExceptionType.generic, - ); - } - - final response = await getAllCurrencies( - fixedRate, - ); - - if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); - } - - return ExchangeResponse( - value: response.value!..removeWhere((e) => e.ticker == forCurrency), - ); - } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); - } catch (e) { - return ExchangeResponse( - exception: ExchangeException( - e.toString(), - ExchangeExceptionType.generic, - ), - ); - } - } - - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - throw UnsupportedError("Not used"); - } + // + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // try { + // if (fixedRate) { + // throw ExchangeException( + // "Nanswap fixedRate not available", + // ExchangeExceptionType.generic, + // ); + // } + // + // final response = await getAllCurrencies( + // fixedRate, + // ); + // + // if (response.exception != null) { + // return ExchangeResponse( + // exception: response.exception, + // ); + // } + // + // return ExchangeResponse( + // value: response.value!..removeWhere((e) => e.ticker == forCurrency), + // ); + // } on ExchangeException catch (e) { + // return ExchangeResponse( + // exception: e, + // ); + // } catch (e) { + // return ExchangeResponse( + // exception: ExchangeException( + // e.toString(), + // ExchangeExceptionType.generic, + // ), + // ); + // } + // } @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { try { @@ -312,9 +294,7 @@ class NanswapExchange extends Exchange { ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -326,9 +306,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -342,14 +320,10 @@ class NanswapExchange extends Exchange { @override Future> getTrade(String tradeId) async { try { - final response = await NanswapAPI.instance.getOrder( - id: tradeId, - ); + final response = await NanswapAPI.instance.getOrder(id: tradeId); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -381,9 +355,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( @@ -406,14 +378,10 @@ class NanswapExchange extends Exchange { @override Future> updateTrade(Trade trade) async { try { - final response = await NanswapAPI.instance.getOrder( - id: trade.tradeId, - ); + final response = await NanswapAPI.instance.getOrder(id: trade.tradeId); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } final t = response.value!; @@ -445,9 +413,7 @@ class NanswapExchange extends Exchange { ), ); } on ExchangeException catch (e) { - return ExchangeResponse( - exception: e, - ); + return ExchangeResponse(exception: e); } catch (e) { return ExchangeResponse( exception: ExchangeException( diff --git a/lib/services/exchange/simpleswap/simpleswap_exchange.dart b/lib/services/exchange/simpleswap/simpleswap_exchange.dart index dae14cbf7..f477d182b 100644 --- a/lib/services/exchange/simpleswap/simpleswap_exchange.dart +++ b/lib/services/exchange/simpleswap/simpleswap_exchange.dart @@ -36,6 +36,8 @@ class SimpleSwapExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -61,28 +63,31 @@ class SimpleSwapExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - final response = - await SimpleSwapAPI.instance.getAllCurrencies(fixedRate: fixedRate); + final response = await SimpleSwapAPI.instance.getAllCurrencies( + fixedRate: fixedRate, + ); if (response.value != null) { - final List currencies = response.value! - .map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.symbol, - name: e.name, - network: e.network, - image: e.image, - externalId: e.extraId, - isFiat: false, - rateType: fixedRate - ? SupportedRateType.both - : SupportedRateType.estimated, - isAvailable: true, - isStackCoin: AppConfig.isStackCoin(e.symbol), - tokenContract: null, - ), - ) - .toList(); + final List currencies = + response.value! + .map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.symbol, + name: e.name, + network: e.network, + image: e.image, + externalId: e.extraId, + isFiat: false, + rateType: + fixedRate + ? SupportedRateType.both + : SupportedRateType.estimated, + isAvailable: true, + isStackCoin: AppConfig.isStackCoin(e.symbol), + tokenContract: null, + ), + ) + .toList(); return ExchangeResponse>( value: currencies, exception: response.exception, @@ -95,15 +100,17 @@ class SimpleSwapExchange extends Exchange { ); } - @override - Future>> getAllPairs(bool fixedRate) async { - return await SimpleSwapAPI.instance.getAllPairs(isFixedRate: fixedRate); - } + // @override + // Future>> getAllPairs(bool fixedRate) async { + // return await SimpleSwapAPI.instance.getAllPairs(isFixedRate: fixedRate); + // } @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, @@ -115,9 +122,7 @@ class SimpleSwapExchange extends Exchange { amount: amount.toString(), ); if (response.exception != null) { - return ExchangeResponse( - exception: response.exception, - ); + return ExchangeResponse(exception: response.exception); } return ExchangeResponse( @@ -135,7 +140,9 @@ class SimpleSwapExchange extends Exchange { @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { return await SimpleSwapAPI.instance.getRange( @@ -145,15 +152,6 @@ class SimpleSwapExchange extends Exchange { ); } - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - // return await SimpleSwapAPI.instance.ge - throw UnimplementedError(); - } - @override Future> getTrade(String tradeId) async { return await SimpleSwapAPI.instance.getExchange(exchangeId: tradeId); diff --git a/lib/services/exchange/trocador/response_objects/trocador_trade.dart b/lib/services/exchange/trocador/response_objects/trocador_trade.dart index 5d1f5d72a..781ef596a 100644 --- a/lib/services/exchange/trocador/response_objects/trocador_trade.dart +++ b/lib/services/exchange/trocador/response_objects/trocador_trade.dart @@ -92,32 +92,37 @@ class TrocadorTrade { ); } + Map toMap() { + return { + "tradeId": tradeId, + "date": date.toIso8601String(), + "tickerFrom": tickerFrom, + "tickerTo": tickerTo, + "coinFrom": coinFrom, + "coinTo": coinTo, + "networkFrom": networkFrom, + "networkTo": networkTo, + "amountFrom": amountFrom.toString(), + "amountTo": amountTo.toString(), + "provider": provider, + "fixed": fixed, + "status": status, + "addressProvider": addressProvider, + "addressProviderMemo": addressProviderMemo, + "addressUser": addressUser, + "addressUserMemo": addressUserMemo, + "refundAddress": refundAddress, + "refundAddressMemo": refundAddressMemo, + "password": password, + "idProvider": idProvider, + "quotes": quotes, + "payment": payment, + }; + } + + @override String toString() { - return 'TrocadorTrade( ' - 'tradeId: $tradeId, ' - 'date: $date, ' - 'tickerFrom: $tickerFrom, ' - 'tickerTo: $tickerTo, ' - 'coinFrom: $coinFrom, ' - 'coinTo: $coinTo, ' - 'networkFrom: $networkFrom, ' - 'networkTo: $networkTo, ' - 'amountFrom: $amountFrom, ' - 'amountTo: $amountTo, ' - 'provider: $provider, ' - 'fixed: $fixed, ' - 'status: $status, ' - 'addressProvider: $addressProvider, ' - 'addressProviderMemo: $addressProviderMemo, ' - 'addressUser: $addressUser, ' - 'addressUserMemo: $addressUserMemo, ' - 'refundAddress: $refundAddress, ' - 'refundAddressMemo: $refundAddressMemo, ' - 'password: $password, ' - 'idProvider: $idProvider, ' - 'quotes: $quotes, ' - 'payment: $payment ' - ')'; + return "TrocadorTrade: ${toMap()}"; } } diff --git a/lib/services/exchange/trocador/trocador_exchange.dart b/lib/services/exchange/trocador/trocador_exchange.dart index cf02cc206..3bae214b6 100644 --- a/lib/services/exchange/trocador/trocador_exchange.dart +++ b/lib/services/exchange/trocador/trocador_exchange.dart @@ -40,6 +40,8 @@ class TrocadorExchange extends Exchange { Future> createTrade({ required String from, required String to, + required String? fromNetwork, + required String? toNetwork, required bool fixedRate, required Decimal amount, required String addressTo, @@ -49,37 +51,38 @@ class TrocadorExchange extends Exchange { Estimate? estimate, required bool reversed, }) async { - final response = reversed - ? await TrocadorAPI.createNewPaymentRateTrade( - isOnion: false, - rateId: estimate?.rateId, - fromTicker: from.toLowerCase(), - fromNetwork: onlySupportedNetwork, - toTicker: to.toLowerCase(), - toNetwork: onlySupportedNetwork, - toAmount: amount.toString(), - receivingAddress: addressTo, - receivingMemo: null, - refundAddress: addressRefund, - refundMemo: null, - exchangeProvider: estimate!.exchangeProvider!, - isFixedRate: fixedRate, - ) - : await TrocadorAPI.createNewStandardRateTrade( - isOnion: false, - rateId: estimate?.rateId, - fromTicker: from.toLowerCase(), - fromNetwork: onlySupportedNetwork, - toTicker: to.toLowerCase(), - toNetwork: onlySupportedNetwork, - fromAmount: amount.toString(), - receivingAddress: addressTo, - receivingMemo: null, - refundAddress: addressRefund, - refundMemo: null, - exchangeProvider: estimate!.exchangeProvider!, - isFixedRate: fixedRate, - ); + final response = + reversed + ? await TrocadorAPI.createNewPaymentRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ) + : await TrocadorAPI.createNewStandardRateTrade( + isOnion: false, + rateId: estimate?.rateId, + fromTicker: from.toLowerCase(), + fromNetwork: onlySupportedNetwork, + toTicker: to.toLowerCase(), + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + receivingAddress: addressTo, + receivingMemo: null, + refundAddress: addressRefund, + refundMemo: null, + exchangeProvider: estimate!.exchangeProvider!, + isFixedRate: fixedRate, + ); if (response.value == null) { return ExchangeResponse(exception: response.exception); @@ -125,22 +128,23 @@ class TrocadorExchange extends Exchange { _cachedCurrencies?.removeWhere((e) => e.network != onlySupportedNetwork); - final value = _cachedCurrencies - ?.map( - (e) => Currency( - exchangeName: exchangeName, - ticker: e.ticker, - name: e.name, - network: e.network, - image: e.image, - isFiat: false, - rateType: SupportedRateType.both, - isStackCoin: AppConfig.isStackCoin(e.ticker), - tokenContract: null, - isAvailable: true, - ), - ) - .toList(); + final value = + _cachedCurrencies + ?.map( + (e) => Currency( + exchangeName: exchangeName, + ticker: e.ticker, + name: e.name, + network: e.network, + image: e.image, + isFiat: false, + rateType: SupportedRateType.both, + isStackCoin: AppConfig.isStackCoin(e.ticker), + tokenContract: null, + isAvailable: true, + ), + ) + .toList(); if (value == null) { return ExchangeResponse( @@ -195,28 +199,31 @@ class TrocadorExchange extends Exchange { @override Future>> getEstimates( String from, + String? fromNetwork, String to, + String? toNetwork, Decimal amount, bool fixedRate, bool reversed, ) async { - final response = reversed - ? await TrocadorAPI.getNewPaymentRate( - isOnion: false, - fromTicker: from, - fromNetwork: onlySupportedNetwork, - toTicker: to, - toNetwork: onlySupportedNetwork, - toAmount: amount.toString(), - ) - : await TrocadorAPI.getNewStandardRate( - isOnion: false, - fromTicker: from, - fromNetwork: onlySupportedNetwork, - toTicker: to, - toNetwork: onlySupportedNetwork, - fromAmount: amount.toString(), - ); + final response = + reversed + ? await TrocadorAPI.getNewPaymentRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + toAmount: amount.toString(), + ) + : await TrocadorAPI.getNewStandardRate( + isOnion: false, + fromTicker: from, + fromNetwork: onlySupportedNetwork, + toTicker: to, + toNetwork: onlySupportedNetwork, + fromAmount: amount.toString(), + ); if (response.value == null) { return ExchangeResponse(exception: response.exception); @@ -265,43 +272,46 @@ class TrocadorExchange extends Exchange { } return ExchangeResponse( - value: estimates - ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), + value: + estimates + ..sort((a, b) => b.estimatedAmount.compareTo(a.estimatedAmount)), ); } - @override - Future>> getPairedCurrencies( - String forCurrency, - bool fixedRate, - ) async { - // TODO: implement getPairedCurrencies - throw UnimplementedError(); - } - - @override - Future>> getPairsFor( - String currency, - bool fixedRate, - ) async { - final response = await getAllPairs(fixedRate); - if (response.value == null) { - return ExchangeResponse(exception: response.exception); - } - - final pairs = response.value!.where( - (e) => - e.from.toUpperCase() == currency.toUpperCase() || - e.to.toUpperCase() == currency.toUpperCase(), - ); - - return ExchangeResponse(value: pairs.toList()); - } + // @override + // Future>> getPairedCurrencies( + // String forCurrency, + // bool fixedRate, + // ) async { + // // TODO: implement getPairedCurrencies + // throw UnimplementedError(); + // } + // + // @override + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ) async { + // final response = await getAllPairs(fixedRate); + // if (response.value == null) { + // return ExchangeResponse(exception: response.exception); + // } + // + // final pairs = response.value!.where( + // (e) => + // e.from.toUpperCase() == currency.toUpperCase() || + // e.to.toUpperCase() == currency.toUpperCase(), + // ); + // + // return ExchangeResponse(value: pairs.toList()); + // } @override Future> getRange( String from, + String? fromNetwork, String to, + String? toNetwork, bool fixedRate, ) async { if (_cachedCurrencies == null) { @@ -316,14 +326,12 @@ class TrocadorExchange extends Exchange { ); } - final fromCoin = _cachedCurrencies! - .firstWhere((e) => e.ticker.toLowerCase() == from.toLowerCase()); + final fromCoin = _cachedCurrencies!.firstWhere( + (e) => e.ticker.toLowerCase() == from.toLowerCase(), + ); return ExchangeResponse( - value: Range( - max: fromCoin.maximum, - min: fromCoin.minimum, - ), + value: Range(max: fromCoin.maximum, min: fromCoin.minimum), ); } diff --git a/lib/services/price.dart b/lib/services/price.dart index 801e720e2..75a4ad664 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -13,7 +13,6 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; -import 'package:tuple/tuple.dart'; import '../app_config.dart'; import '../db/hive/db.dart'; @@ -54,13 +53,15 @@ class PriceAPI { static const refreshInterval = 60; // initialize to older than current time minus at least refreshInterval - static DateTime _lastCalled = - DateTime.now().subtract(const Duration(seconds: refreshInterval + 10)); + static DateTime _lastCalled = DateTime.now().subtract( + const Duration(seconds: refreshInterval + 10), + ); static String _lastUsedBaseCurrency = ""; - static const Duration refreshIntervalDuration = - Duration(seconds: refreshInterval); + static const Duration refreshIntervalDuration = Duration( + seconds: refreshInterval, + ); final HTTP client; @@ -72,7 +73,7 @@ class PriceAPI { } Future _updateCachedPrices( - Map> data, + Map data, ) async { final Map map = {}; @@ -81,30 +82,29 @@ class PriceAPI { if (entry == null) { map[coin.prettyName] = ["0", 0.0]; } else { - map[coin.prettyName] = [entry.item1.toString(), entry.item2]; + map[coin.prettyName] = [entry.value.toString(), entry.change24h]; } } - await DB.instance - .put(boxName: DB.boxNamePriceCache, key: 'cache', value: map); + await DB.instance.put( + boxName: DB.boxNamePriceCache, + key: 'cache', + value: map, + ); } - Map> get _cachedPrices { + Map get _cachedPrices { final map = DB.instance.get(boxName: DB.boxNamePriceCache, key: 'cache') - as Map? ?? - {}; + as Map? ?? + {}; // init with 0 - final result = { - for (final coin in AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0), - }; + final Map result = {}; for (final entry in map.entries) { - result[AppConfig.getCryptoCurrencyByPrettyName( - entry.key as String, - )] = Tuple2( - Decimal.parse(entry.value[0] as String), - entry.value[1] as double, + result[AppConfig.getCryptoCurrencyByPrettyName(entry.key as String)] = ( + value: Decimal.parse(entry.value[0] as String), + change24h: entry.value[1] as double, ); } @@ -117,9 +117,8 @@ class PriceAPI { .where((e) => e != null) .join(","); - Future>> getPricesAnd24hChange({ - required String baseCurrency, - }) async { + Future> + getPricesAnd24hChange({required String baseCurrency}) async { final now = DateTime.now(); if (_lastUsedBaseCurrency != baseCurrency || now.difference(_lastCalled) > refreshIntervalDuration) { @@ -132,12 +131,10 @@ class PriceAPI { final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return _cachedPrices; } - final Map> result = {}; + final Map result = {}; try { final uri = Uri.parse( "https://api.coingecko.com/api/v3/coins/markets?vs_currency" @@ -148,9 +145,10 @@ class PriceAPI { final coinGeckoResponse = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final coinGeckoData = jsonDecode(coinGeckoResponse.body) as List; @@ -159,12 +157,17 @@ class PriceAPI { final String coinName = map["name"] as String; final coin = AppConfig.getCryptoCurrencyByPrettyName(coinName); - final price = Decimal.parse(map["current_price"].toString()); - final change24h = map["price_change_percentage_24h"] != null - ? double.parse(map["price_change_percentage_24h"].toString()) - : 0.0; - - result[coin] = Tuple2(price, change24h); + try { + final price = Decimal.parse(map["current_price"].toString()); + final change24h = + map["price_change_percentage_24h"] != null + ? double.parse(map["price_change_percentage_24h"].toString()) + : 0.0; + + result[coin] = (value: price, change24h: change24h); + } catch (_) { + result.remove(coin); + } } // update cache @@ -172,8 +175,11 @@ class PriceAPI { return _cachedPrices; } catch (e, s) { - Logging.instance - .e("getPricesAnd24hChange($baseCurrency): ", error: e, stackTrace: s); + Logging.instance.e( + "getPricesAnd24hChange($baseCurrency): ", + error: e, + stackTrace: s, + ); // return previous cached values return _cachedPrices; } @@ -185,9 +191,7 @@ class PriceAPI { if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return null; } const uriString = @@ -197,9 +201,10 @@ class PriceAPI { final response = await client.get( url: uri, headers: {'Content-Type': 'application/json'}, - proxyInfo: Prefs.instance.useTor - ? TorService.sharedInstance.getProxyInfo() - : null, + proxyInfo: + Prefs.instance.useTor + ? TorService.sharedInstance.getProxyInfo() + : null, ); final json = jsonDecode(response.body) as List; @@ -214,22 +219,22 @@ class PriceAPI { } } - Future>> - getPricesAnd24hChangeForEthTokens({ + Future> + getPricesAnd24hChangeForEthTokens({ required Set contractAddresses, required String baseCurrency, }) async { - final Map> tokenPrices = {}; + final Map tokenPrices = {}; if (AppConfig.coins.whereType().isEmpty || - contractAddresses.isEmpty) return tokenPrices; + contractAddresses.isEmpty) { + return tokenPrices; + } final externalCalls = Prefs.instance.externalCalls; if ((!Util.isTestEnv && !externalCalls) || !(await Prefs.instance.isExternalCallsSet())) { - Logging.instance.i( - "User does not want to use external calls", - ); + Logging.instance.i("User does not want to use external calls"); return tokenPrices; } diff --git a/lib/services/price_service.dart b/lib/services/price_service.dart index fb30c8477..43eb68e32 100644 --- a/lib/services/price_service.dart +++ b/lib/services/price_service.dart @@ -13,13 +13,12 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; + import '../db/isar/main_db.dart'; import '../models/isar/models/isar_models.dart'; import '../networking/http.dart'; -import 'price.dart'; -import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; -import 'package:tuple/tuple.dart'; +import 'price.dart'; class PriceService extends ChangeNotifier { late final String baseTicker; @@ -29,25 +28,26 @@ class PriceService extends ChangeNotifier { final Duration updateInterval = const Duration(seconds: 60); Timer? _timer; - final Map> _cachedPrices = { - for (final coin in AppConfig.coins) coin: Tuple2(Decimal.zero, 0.0), - }; + final Map _cachedPrices = + {}; - final Map> _cachedTokenPrices = {}; + final Map _cachedTokenPrices = + {}; final _priceAPI = PriceAPI(HTTP()); - Tuple2 getPrice(CryptoCurrency coin) => _cachedPrices[coin]!; + ({Decimal value, double change24h})? getPrice(CryptoCurrency coin) => + _cachedPrices[coin]; - Tuple2 getTokenPrice(String contractAddress) => - _cachedTokenPrices[contractAddress.toLowerCase()] ?? - Tuple2(Decimal.zero, 0); + ({Decimal value, double change24h})? getTokenPrice(String contractAddress) => + _cachedTokenPrices[contractAddress.toLowerCase()]; PriceService(this.baseTicker); Future updatePrice() async { - final priceMap = - await _priceAPI.getPricesAnd24hChange(baseCurrency: baseTicker); + final priceMap = await _priceAPI.getPricesAnd24hChange( + baseCurrency: baseTicker, + ); bool shouldNotify = false; for (final map in priceMap.entries) { 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/services/trade_service.dart b/lib/services/trade_service.dart index 6207b102e..e15216a33 100644 --- a/lib/services/trade_service.dart +++ b/lib/services/trade_service.dart @@ -38,8 +38,11 @@ class TradesService extends ChangeNotifier { required Trade trade, required bool shouldNotifyListeners, }) async { - await DB.instance - .put(boxName: DB.boxNameTradesV2, key: trade.uuid, value: trade); + await DB.instance.put( + boxName: DB.boxNameTradesV2, + key: trade.uuid, + value: trade, + ); if (shouldNotifyListeners) { notifyListeners(); diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index 171dc09b2..d14e4e809 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -33,8 +33,9 @@ class AddressUtils { /// Return only recognized parameters. static Map _filterParams(Map params) { return Map.fromEntries( - params.entries - .where((entry) => recognizedParams.contains(entry.key.toLowerCase())), + params.entries.where( + (entry) => recognizedParams.contains(entry.key.toLowerCase()), + ), ); } @@ -52,8 +53,10 @@ class AddressUtils { result["address"] = u.path; } else if (result["scheme"] == "monero") { // Monero addresses can contain '?' which Uri.parse interprets as query start. - final addressEnd = - uri.indexOf('?', 7); // 7 is the length of "monero:". + final addressEnd = uri.indexOf( + '?', + 7, + ); // 7 is the length of "monero:". if (addressEnd != -1) { result["address"] = uri.substring(7, addressEnd); } else { @@ -130,10 +133,7 @@ class AddressUtils { /// Centralized method to handle various cryptocurrency URIs and return a common object. /// /// Returns null on failure to parse - static PaymentUriData? parsePaymentUri( - String uri, { - Logging? logging, - }) { + static PaymentUriData? parsePaymentUri(String uri, {Logging? logging}) { try { final Map parsedData = _parseUri(uri); @@ -155,7 +155,7 @@ class AddressUtils { additionalParams: filteredParams, ); } catch (e, s) { - logging?.e("", error: e, stackTrace: s); + logging?.i("Invalid payment URI: $uri", error: e, stackTrace: s); return null; } } @@ -259,8 +259,8 @@ class PaymentUriData { final Map additionalParams; CryptoCurrency? get coin => AddressUtils._getCryptoCurrencyByScheme( - scheme ?? "", // empty will just return null - ); + scheme ?? "", // empty will just return null + ); PaymentUriData({ required this.address, @@ -273,7 +273,8 @@ class PaymentUriData { }); @override - String toString() => "PaymentUriData { " + String toString() => + "PaymentUriData { " "coin: $coin, " "address: $address, " "amount: $amount, " diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 73c520410..c12f215c0 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -62,8 +62,10 @@ class _EXCHANGE { case NanswapExchange.exchangeName: return nanswap; default: - throw ArgumentError("Invalid exchange name passed to " - "Assets.exchange.getIconFor()"); + throw ArgumentError( + "Invalid exchange name passed to " + "Assets.exchange.getIconFor()", + ); } } } @@ -232,7 +234,7 @@ class _SVG { String get trocadorRatingC => "assets/svg/trocador_rating_c.svg"; String get trocadorRatingD => "assets/svg/trocador_rating_d.svg"; -// TODO provide proper assets + // TODO provide proper assets String get bitcoinTestnet => "assets/svg/coin_icons/Bitcoin.svg"; String get bitcoincashTestnet => "assets/svg/coin_icons/Bitcoincash.svg"; String get firoTestnet => "assets/svg/coin_icons/Firo.svg"; diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart index c105412db..3bb8557de 100644 --- a/lib/utilities/eth_commons.dart +++ b/lib/utilities/eth_commons.dart @@ -12,36 +12,45 @@ import 'package:bip32/bip32.dart' as bip32; import 'package:bip39/bip39.dart' as bip39; import 'package:decimal/decimal.dart'; import "package:hex/hex.dart"; + import '../wallets/crypto_currency/crypto_currency.dart'; class GasTracker { + final Decimal low; final Decimal average; - final Decimal fast; - final Decimal slow; + final Decimal high; final int numberOfBlocksFast; final int numberOfBlocksAverage; final int numberOfBlocksSlow; + final Decimal suggestBaseFee; + final String lastBlock; const GasTracker({ + required this.low, required this.average, - required this.fast, - required this.slow, + required this.high, required this.numberOfBlocksFast, required this.numberOfBlocksAverage, required this.numberOfBlocksSlow, + required this.suggestBaseFee, required this.lastBlock, }); + Decimal get lowPriority => (low - suggestBaseFee).zeroIfNegative; + Decimal get averagePriority => (average - suggestBaseFee).zeroIfNegative; + Decimal get highPriority => (high - suggestBaseFee).zeroIfNegative; + factory GasTracker.fromJson(Map json) { final targetTime = Ethereum(CryptoCurrencyNetwork.main).targetBlockTimeSeconds; return GasTracker( - fast: Decimal.parse(json["FastGasPrice"].toString()), + high: Decimal.parse(json["FastGasPrice"].toString()), average: Decimal.parse(json["ProposeGasPrice"].toString()), - slow: Decimal.parse(json["SafeGasPrice"].toString()), + low: Decimal.parse(json["SafeGasPrice"].toString()), + suggestBaseFee: Decimal.parse(json["suggestBaseFee"].toString()), // TODO fix hardcoded numberOfBlocksFast: 30 ~/ targetTime, numberOfBlocksAverage: 180 ~/ targetTime, @@ -49,10 +58,34 @@ class GasTracker { lastBlock: json["LastBlock"] as String, ); } + + @override + String toString() { + return 'GasTracker(' + 'slow: $low, ' + 'average: $average, ' + 'fast: $high, ' + 'suggestBaseFee: $suggestBaseFee, ' + 'numberOfBlocksFast: $numberOfBlocksFast, ' + 'numberOfBlocksAverage: $numberOfBlocksAverage, ' + 'numberOfBlocksSlow: $numberOfBlocksSlow, ' + 'lastBlock: $lastBlock' + ')'; + } +} + +extension DecimalExt on Decimal { + Decimal get zeroIfNegative { + if (this < Decimal.zero) return Decimal.zero; + return this; + } } const hdPathEthereum = "m/44'/60'/0'/0"; +const kEthereumMinGasLimit = 21000; +const kEthereumTokenMinGasLimit = 65000; + // equal to "0x${keccak256("Transfer(address,address,uint256)".toUint8ListFromUtf8).toHex}"; const kTransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; diff --git a/lib/utilities/logger.dart b/lib/utilities/logger.dart index f62b1ea4e..d4cc03dcf 100644 --- a/lib/utilities/logger.dart +++ b/lib/utilities/logger.dart @@ -53,87 +53,79 @@ class Logging { SendPort get _sendPort { final port = IsolateNameServer.lookupPortByName(_kLoggerPortName); if (port == null) { - throw Exception( - "Did you forget to call Logging.initialize()?", - ); + throw Exception("Did you forget to call Logging.initialize()?"); } return port; } - Future initialize(String logsPath, {required Level level}) async { + Future initialize( + String logsPath, { + required Level level, + Level? debugConsoleLevel, + }) async { if (Isolate.current.debugName != "main") { throw Exception( "Logging.initialize() must be called on the main isolate.", ); } if (IsolateNameServer.lookupPortByName(_kLoggerPortName) != null) { - throw Exception( - "Logging was already initialized", - ); + throw Exception("Logging was already initialized"); } logsDirPath = logsPath; final receivePort = ReceivePort(); - await Isolate.spawn( - (sendPort) { - final ReceivePort receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); + await Isolate.spawn((sendPort) { + final ReceivePort receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + + PrettyPrinter prettyPrinter(bool toFile) => PrettyPrinter( + printEmojis: false, + methodCount: 0, + dateTimeFormat: + toFile ? DateTimeFormat.none : DateTimeFormat.dateAndTime, + colors: !toFile, + noBoxingByDefault: toFile, + ); - PrettyPrinter prettyPrinter(bool toFile) => PrettyPrinter( - printEmojis: false, - methodCount: 0, - dateTimeFormat: - toFile ? DateTimeFormat.none : DateTimeFormat.dateAndTime, - colors: !toFile, - noBoxingByDefault: toFile, - ); + final consoleLogger = Logger( + printer: PrefixPrinter(prettyPrinter(false)), + filter: ProductionFilter(), + level: debugConsoleLevel ?? level, + ); - final consoleLogger = Logger( - printer: PrefixPrinter(prettyPrinter(false)), - filter: ProductionFilter(), - level: level, - ); + final fileLogger = Logger( + printer: PrefixPrinter(prettyPrinter(true)), + filter: ProductionFilter(), + level: level, + output: AdvancedFileOutput( + path: logsDirPath, + overrideExisting: false, + latestFileName: "latest.txt", + writeImmediately: [Level.error, Level.fatal, Level.warning], + ), + ); - final fileLogger = Logger( - printer: PrefixPrinter(prettyPrinter(true)), - filter: ProductionFilter(), - level: level, - output: AdvancedFileOutput( - path: logsDirPath, - overrideExisting: false, - latestFileName: "latest.txt", - writeImmediately: [ - Level.error, - Level.fatal, - Level.warning, - Level.trace, // mainly for spark debugging. TODO: Remove later - ], - ), + receivePort.listen((message) { + final event = (message as (LogEvent, bool)).$1; + consoleLogger.log( + event.level, + event.message, + stackTrace: event.stackTrace, + error: event.error, + time: event.time.toUtc(), ); - - receivePort.listen((message) { - final event = (message as (LogEvent, bool)).$1; - consoleLogger.log( + if (message.$2) { + fileLogger.log( event.level, - event.message, + "${event.time.toUtc().toIso8601String()} ${event.message}", stackTrace: event.stackTrace, error: event.error, - time: event.time.toUtc(), + time: event.time, ); - if (message.$2) { - fileLogger.log( - event.level, - "${event.time.toUtc().toIso8601String()} ${event.message}", - stackTrace: event.stackTrace, - error: event.error, - time: event.time, - ); - } - }); - }, - receivePort.sendPort, - ); + } + }); + }, receivePort.sendPort); final loggerPort = await receivePort.first as SendPort; IsolateNameServer.registerPortWithName(loggerPort, _kLoggerPortName); } @@ -155,18 +147,16 @@ class Logging { toFile = false; } try { - _sendPort.send( - ( - LogEvent( - level, - _stringifyMessage(message), - time: time, - error: error, - stackTrace: stackTrace, - ), - toFile + _sendPort.send(( + LogEvent( + level, + _stringifyMessage(message), + time: time, + error: error, + stackTrace: stackTrace, ), - ); + toFile, + )); } catch (e, s) { t("Isolates suck", error: e, stackTrace: s); } @@ -177,82 +167,76 @@ class Logging { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.trace, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.trace, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void d( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.debug, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.debug, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void i( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.info, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.info, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void w( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.warning, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.warning, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void e( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.error, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.error, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); void f( dynamic message, { DateTime? time, Object? error, StackTrace? stackTrace, - }) => - log( - Level.fatal, - message, - time: time, - error: error, - stackTrace: stackTrace, - ); + }) => log( + Level.fatal, + message, + time: time, + error: error, + stackTrace: stackTrace, + ); } 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/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index 1d7bb53ed..cd908b9e6 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -4,6 +4,7 @@ import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/node_model.dart'; import '../../../utilities/default_nodes.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/eth_commons.dart'; import '../crypto_currency.dart'; import '../intermediate/bip39_currency.dart'; @@ -41,25 +42,25 @@ class Ethereum extends Bip39Currency { @override String get ticker => _ticker; - int get gasLimit => 21000; + int get gasLimit => kEthereumMinGasLimit; @override bool get hasTokenSupport => true; @override NodeModel get defaultNode => NodeModel( - host: "https://eth.stackwallet.com", - port: 443, - name: DefaultNodes.defaultName, - id: DefaultNodes.buildId(this), - useSSL: true, - enabled: true, - coinName: identifier, - isFailover: true, - isDown: false, - torEnabled: true, - clearnetEnabled: true, - ); + host: "https://eth2.stackwallet.com", + port: 443, + name: DefaultNodes.defaultName, + id: DefaultNodes.buildId(this), + useSSL: true, + enabled: true, + coinName: identifier, + isFailover: true, + isDown: false, + torEnabled: true, + clearnetEnabled: true, + ); @override // Not used for eth 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..c95fad163 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -7,6 +7,7 @@ import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../widgets/eth_fee_form.dart'; import '../isar/models/spark_coin.dart'; import 'name_op_state.dart'; @@ -14,7 +15,7 @@ typedef TxRecipient = ({String address, Amount amount, bool isChange}); class TxData { final FeeRateType? feeRateType; - final int? feeRateAmount; + final BigInt? feeRateAmount; final int? satsPerVByte; final Amount? fee; @@ -43,12 +44,11 @@ class TxData { // paynym specific final PaynymAccountLite? paynymAccountLite; - // eth token specific + // eth & token specific + final EthEIP1559Fee? ethEIP1559Fee; final web3dart.Transaction? web3dartTransaction; final int? nonce; final BigInt? chainId; - final BigInt? feeInWei; - // wownero and monero specific final lib_monero.PendingTransaction? pendingTransaction; @@ -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; @@ -103,10 +105,10 @@ class TxData { this.frostMSConfig, this.frostSigners, this.paynymAccountLite, + this.ethEIP1559Fee, this.web3dartTransaction, this.nonce, this.chainId, - this.feeInWei, this.pendingTransaction, this.jMintValue, this.spendCoinIndexes, @@ -122,6 +124,7 @@ class TxData { this.tempTx, this.ignoreCachedBalanceChecks = false, this.opNameState, + this.sparkNameInfo, }); Amount? get amount { @@ -201,13 +204,14 @@ 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, - int? feeRateAmount, + BigInt? feeRateAmount, int? satsPerVByte, Amount? fee, int? vSize, @@ -225,10 +229,10 @@ class TxData { List? frostSigners, String? changeAddress, PaynymAccountLite? paynymAccountLite, + EthEIP1559Fee? ethEIP1559Fee, web3dart.Transaction? web3dartTransaction, int? nonce, BigInt? chainId, - BigInt? feeInWei, lib_monero.PendingTransaction? pendingTransaction, int? jMintValue, List? spendCoinIndexes, @@ -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, @@ -271,10 +276,10 @@ class TxData { frostSigners: frostSigners ?? this.frostSigners, changeAddress: changeAddress ?? this.changeAddress, paynymAccountLite: paynymAccountLite ?? this.paynymAccountLite, + ethEIP1559Fee: ethEIP1559Fee ?? this.ethEIP1559Fee, web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction, nonce: nonce ?? this.nonce, chainId: chainId ?? this.chainId, - feeInWei: feeInWei ?? this.feeInWei, pendingTransaction: pendingTransaction ?? this.pendingTransaction, jMintValue: jMintValue ?? this.jMintValue, spendCoinIndexes: spendCoinIndexes ?? this.spendCoinIndexes, @@ -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, ' @@ -312,10 +319,10 @@ class TxData { 'frostSigners: $frostSigners, ' 'changeAddress: $changeAddress, ' 'paynymAccountLite: $paynymAccountLite, ' + 'ethEIP1559Fee: $ethEIP1559Fee, ' 'web3dartTransaction: $web3dartTransaction, ' 'nonce: $nonce, ' 'chainId: $chainId, ' - 'feeInWei: $feeInWei, ' 'pendingTransaction: $pendingTransaction, ' 'jMintValue: $jMintValue, ' 'spendCoinIndexes: $spendCoinIndexes, ' @@ -331,5 +338,6 @@ class TxData { 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' + 'sparkNameInfo: $sparkNameInfo, ' '}'; } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 3beaa0e8d..2b5173b6b 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -337,7 +337,7 @@ class BitcoinFrostWallet extends Wallet } } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(BigInt feeRate) async { int available = 0; int inputCount = 0; final height = await chainHeight; @@ -367,11 +367,11 @@ class BitcoinFrostWallet extends Wallet // return vSize * (feeRatePerKB / 1000).ceil(); // } - Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount _roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -724,7 +724,7 @@ class BitcoinFrostWallet extends Wallet } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final available = info.cachedBalance.spendable; if (available == amount) { @@ -790,15 +790,15 @@ class BitcoinFrostWallet extends Wallet fast: Amount.fromDecimal( fast, fractionDigits: cryptoCurrency.fractionDigits, - ).raw.toInt(), + ).raw, medium: Amount.fromDecimal( medium, fractionDigits: cryptoCurrency.fractionDigits, - ).raw.toInt(), + ).raw, slow: Amount.fromDecimal( slow, fractionDigits: cryptoCurrency.fractionDigits, - ).raw.toInt(), + ).raw, ); Logging.instance.i("fetched fees: $feeObject"); diff --git a/lib/wallets/wallet/impl/bitcoin_wallet.dart b/lib/wallets/wallet/impl/bitcoin_wallet.dart index 5c7f3ed75..2ce942673 100644 --- a/lib/wallets/wallet/impl/bitcoin_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_wallet.dart @@ -54,19 +54,19 @@ class BitcoinWallet extends Bip39HDWallet // =========================================================================== @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // // @override diff --git a/lib/wallets/wallet/impl/bitcoincash_wallet.dart b/lib/wallets/wallet/impl/bitcoincash_wallet.dart index 666d5fd6b..cee690219 100644 --- a/lib/wallets/wallet/impl/bitcoincash_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoincash_wallet.dart @@ -33,57 +33,54 @@ class BitcoincashWallet int get isarTransactionVersion => 2; BitcoincashWallet(CryptoCurrencyNetwork network) - : super(Bitcoincash(network) as T); + : super(Bitcoincash(network) as T); @override - FilterOperation? get changeAddressFilterOperation => FilterGroup.and( - [ - ...standardChangeAddressFilters, - FilterGroup.not( - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/0'", - ), - ), - ), - ], - ); + FilterOperation? get changeAddressFilterOperation => FilterGroup.and([ + ...standardChangeAddressFilters, + FilterGroup.not( + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith( + property: "value", + value: "m/44'/0'", + ), + ), + ), + ]); @override - FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( - [ - ...standardReceivingAddressFilters, - FilterGroup.not( - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/0'", - ), - ), - ), - ], - ); + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and([ + ...standardReceivingAddressFilters, + FilterGroup.not( + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith( + property: "value", + value: "m/44'/0'", + ), + ), + ), + ]); // =========================================================================== @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .group( - (q) => q - .subTypeEqualTo(AddressSubType.receiving) - .or() - .subTypeEqualTo(AddressSubType.change), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group( + (q) => q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.change), + ) + .findAll(); return allAddresses; } @@ -106,20 +103,23 @@ class BitcoincashWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); final List> allTransactions = []; @@ -139,8 +139,9 @@ class BitcoincashWallet ); // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -294,12 +295,8 @@ class BitcoincashWallet // only found outputs owned by this wallet type = TransactionType.incoming; } else { - Logging.instance.e( - "Unexpected tx found (ignoring it)", - ); - Logging.instance.d( - "Unexpected tx found (ignoring it): $txData", - ); + Logging.instance.e("Unexpected tx found (ignoring it)"); + Logging.instance.d("Unexpected tx found (ignoring it): $txData"); continue; } @@ -310,7 +307,8 @@ class BitcoincashWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -327,7 +325,7 @@ class BitcoincashWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -339,8 +337,9 @@ class BitcoincashWallet if (scriptPubKeyHex != null) { // check for cash tokens try { - final ctOutput = - cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex); + final ctOutput = cash_tokens.unwrap_spk( + scriptPubKeyHex.toUint8ListFromHex, + ); if (ctOutput.token_data != null) { // found a token! blocked = true; @@ -374,19 +373,23 @@ class BitcoincashWallet // TODO: correct formula for bch? @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: info.coin.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 3beafe5f5..2cad7c514 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -45,15 +45,16 @@ class CardanoWallet extends Bip39Wallet { final seed = CardanoIcarusSeedGenerator(mnemonic).generate(); final cip1852 = Cip1852.fromSeed(seed, Cip1852Coins.cardanoIcarus); final derivationAccount = cip1852.purpose.coin.account(0); - final shelley = CardanoShelley.fromCip1852Object(derivationAccount) - .change(Bip44Changes.chainExt) - .addressIndex(0); + final shelley = CardanoShelley.fromCip1852Object( + derivationAccount, + ).change(Bip44Changes.chainExt).addressIndex(0); final paymentPublicKey = shelley.bip44.publicKey.compressed; final stakePublicKey = shelley.bip44Sk.publicKey.compressed; - final addressStr = ADABaseAddress.fromPublicKey( - basePubkeyBytes: paymentPublicKey, - stakePubkeyBytes: stakePublicKey, - ).address; + final addressStr = + ADABaseAddress.fromPublicKey( + basePubkeyBytes: paymentPublicKey, + stakePubkeyBytes: stakePublicKey, + ).address; return Address( walletId: walletId, value: addressStr, @@ -76,7 +77,11 @@ class CardanoWallet extends Bip39Wallet { await mainDB.updateOrPutAddresses([address]); } } catch (e, s) { - Logging.instance.e("$runtimeType checkSaveInitialReceivingAddress() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -91,13 +96,17 @@ class CardanoWallet extends Bip39Wallet { return Future.value(health); } catch (e, s) { - Logging.instance.e("Error ping checking in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error ping checking in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); return Future.value(false); } } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { await updateProvider(); if (info.cachedBalance.spendable.raw == BigInt.zero) { @@ -113,10 +122,7 @@ class CardanoWallet extends Bip39Wallet { final fee = params.calculateFee(284); - return Amount( - rawValue: fee, - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount(rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits); } @override @@ -129,7 +135,7 @@ class CardanoWallet extends Bip39Wallet { ); // 284 is the size of a basic transaction with one input and two outputs (change and recipient) - final fee = params.calculateFee(284).toInt(); + final fee = params.calculateFee(284); return FeeObject( numberOfBlocksFast: 2, @@ -140,7 +146,11 @@ class CardanoWallet extends Bip39Wallet { slow: fee, ); } catch (e, s) { - Logging.instance.e("Error getting fees in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting fees in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -181,39 +191,45 @@ class CardanoWallet extends Bip39Wallet { } final bip32 = CardanoIcarusBip32.fromSeed( - CardanoIcarusSeedGenerator(await getMnemonic()).generate()); + CardanoIcarusSeedGenerator(await getMnemonic()).generate(), + ); final spend = bip32.derivePath("1852'/1815'/0'/0/0"); final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw); // Calculate fees with example tx final exampleFee = ADAHelper.toLovelaces("0.10"); final change = TransactionOutput( - address: ADABaseAddress((await getCurrentReceivingAddress())!.value), - amount: Value(coin: totalBalance - (txData.amount!.raw))); + address: ADABaseAddress((await getCurrentReceivingAddress())!.value), + amount: Value(coin: totalBalance - (txData.amount!.raw)), + ); final body = TransactionBody( - inputs: listOfUtxosToBeUsed - .map((e) => TransactionInput( - transactionId: TransactionHash.fromHex(e.txHash), - index: e.outputIndex)) - .toList(), + inputs: + listOfUtxosToBeUsed + .map( + (e) => TransactionInput( + transactionId: TransactionHash.fromHex(e.txHash), + index: e.outputIndex, + ), + ) + .toList(), outputs: [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw - exampleFee)) + address: ADABaseAddress(txData.recipients!.first.address), + amount: Value(coin: txData.amount!.raw - exampleFee), + ), ], fee: exampleFee, ); final exampleTx = ADATransaction( body: body, witnessSet: TransactionWitnessSet( - vKeys: [ - privateKey.createSignatureWitness(body.toHash().data), - ], + vKeys: [privateKey.createSignatureWitness(body.toHash().data)], ), ); - final params = await blockfrostProvider! - .request(BlockfrostRequestLatestEpochProtocolParameters()); + final params = await blockfrostProvider!.request( + BlockfrostRequestLatestEpochProtocolParameters(), + ); final fee = params.calculateFee(exampleTx.size); // Check if we are sending all balance, which means no change and only one output for recipient. @@ -244,7 +260,8 @@ class CardanoWallet extends Bip39Wallet { if (totalBalance - (txData.amount!.raw + fee) < ADAHelper.toLovelaces("1")) { throw Exception( - "Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change."); + "Not enough balance for change. By network rules, please either send all balance or leave at least 1 ADA change.", + ); } return txData.copyWith( @@ -255,7 +272,11 @@ class CardanoWallet extends Bip39Wallet { ); } } catch (e, s) { - Logging.instance.e("$runtimeType Cardano prepareSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Cardano prepareSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -293,44 +314,51 @@ class CardanoWallet extends Bip39Wallet { } final bip32 = CardanoIcarusBip32.fromSeed( - CardanoIcarusSeedGenerator(await getMnemonic()).generate()); + CardanoIcarusSeedGenerator(await getMnemonic()).generate(), + ); final spend = bip32.derivePath("1852'/1815'/0'/0/0"); final privateKey = AdaPrivateKey.fromBytes(spend.privateKey.raw); final change = TransactionOutput( - address: ADABaseAddress((await getCurrentReceivingAddress())!.value), - amount: Value( - coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw))); + address: ADABaseAddress((await getCurrentReceivingAddress())!.value), + amount: Value( + coin: totalUtxoAmount - (txData.amount!.raw + txData.fee!.raw), + ), + ); List outputs = []; if (totalBalance == (txData.amount!.raw + txData.fee!.raw)) { outputs = [ TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw)) + address: ADABaseAddress(txData.recipients!.first.address), + amount: Value(coin: txData.amount!.raw), + ), ]; } else { outputs = [ change, TransactionOutput( - address: ADABaseAddress(txData.recipients!.first.address), - amount: Value(coin: txData.amount!.raw)) + address: ADABaseAddress(txData.recipients!.first.address), + amount: Value(coin: txData.amount!.raw), + ), ]; } final body = TransactionBody( - inputs: listOfUtxosToBeUsed - .map((e) => TransactionInput( - transactionId: TransactionHash.fromHex(e.txHash), - index: e.outputIndex)) - .toList(), + inputs: + listOfUtxosToBeUsed + .map( + (e) => TransactionInput( + transactionId: TransactionHash.fromHex(e.txHash), + index: e.outputIndex, + ), + ) + .toList(), outputs: outputs, fee: txData.fee!.raw, ); final tx = ADATransaction( body: body, witnessSet: TransactionWitnessSet( - vKeys: [ - privateKey.createSignatureWitness(body.toHash().data), - ], + vKeys: [privateKey.createSignatureWitness(body.toHash().data)], ), ); @@ -339,11 +367,13 @@ class CardanoWallet extends Bip39Wallet { transactionCborBytes: tx.serialize(), ), ); - return txData.copyWith( - txid: sentTx, - ); + return txData.copyWith(txid: sentTx); } catch (e, s) { - Logging.instance.e("$runtimeType Cardano confirmSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Cardano confirmSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -410,7 +440,11 @@ class CardanoWallet extends Bip39Wallet { await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("Error getting balance in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting balance in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -428,7 +462,11 @@ class CardanoWallet extends Bip39Wallet { isar: mainDB.isar, ); } catch (e, s) { - Logging.instance.e("Error updating transactions in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error updating transactions in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -446,14 +484,13 @@ class CardanoWallet extends Bip39Wallet { final txsList = await blockfrostProvider!.request( BlockfrostRequestAddressTransactions( - ADAAddress.fromAddress( - currentAddr, - ), + ADAAddress.fromAddress(currentAddr), ), ); - final parsedTxsList = - List>.empty(growable: true); + final parsedTxsList = List>.empty( + growable: true, + ); for (final tx in txsList) { final txInfo = await blockfrostProvider!.request( @@ -525,10 +562,11 @@ class CardanoWallet extends Bip39Wallet { type: txType, subType: isar.TransactionSubType.none, amount: amount, - amountString: Amount( - rawValue: BigInt.from(amount), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(amount), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), fee: int.parse(txInfo.fees), height: txInfo.blockHeight, isCancelled: false, @@ -548,9 +586,10 @@ class CardanoWallet extends Bip39Wallet { derivationIndex: 0, derivationPath: DerivationPath()..value = _addressDerivationPath, type: AddressType.cardanoShelley, - subType: txType == isar.TransactionType.outgoing - ? AddressSubType.unknown - : AddressSubType.receiving, + subType: + txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving, ); parsedTxsList.add(Tuple2(transaction, txAddress)); @@ -560,7 +599,11 @@ class CardanoWallet extends Bip39Wallet { } on NodeTorMismatchConfigException { rethrow; } catch (e, s) { - Logging.instance.e("Error updating transactions in cardano_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error updating transactions in cardano_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -576,10 +619,7 @@ class CardanoWallet extends Bip39Wallet { final client = HttpClient(); if (prefs.useTor) { final proxyInfo = TorService.sharedInstance.getProxyInfo(); - final proxySettings = ProxySettings( - proxyInfo.host, - proxyInfo.port, - ); + final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port); SocksTCPClient.assignToHttpClient(client, [proxySettings]); } blockfrostProvider = CustomBlockForestProvider( diff --git a/lib/wallets/wallet/impl/dash_wallet.dart b/lib/wallets/wallet/impl/dash_wallet.dart index 59fdaa037..f9a90d5b8 100644 --- a/lib/wallets/wallet/impl/dash_wallet.dart +++ b/lib/wallets/wallet/impl/dash_wallet.dart @@ -36,17 +36,18 @@ class DashWallet extends Bip39HDWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -59,30 +60,34 @@ class DashWallet extends Bip39HDWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -95,8 +100,9 @@ class DashWallet extends Bip39HDWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -246,7 +252,8 @@ class DashWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -263,7 +270,7 @@ class DashWallet extends Bip39HDWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -296,18 +303,22 @@ class DashWallet extends Bip39HDWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } } diff --git a/lib/wallets/wallet/impl/dogecoin_wallet.dart b/lib/wallets/wallet/impl/dogecoin_wallet.dart index e9046f959..da24c5a9d 100644 --- a/lib/wallets/wallet/impl/dogecoin_wallet.dart +++ b/lib/wallets/wallet/impl/dogecoin_wallet.dart @@ -38,17 +38,18 @@ class DogecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -61,30 +62,34 @@ class DogecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -97,8 +102,9 @@ class DogecoinWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -249,7 +255,8 @@ class DogecoinWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -266,7 +273,7 @@ class DogecoinWallet @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -287,7 +294,8 @@ class DogecoinWallet // https://en.bitcoin.it/wiki/BIP_0047#Sending if (bytes.length == 80 && bytes.first == 1) { blocked = true; - blockedReason = "Paynym notification output. Incautious " + blockedReason = + "Paynym notification output. Incautious " "handling of outputs from notification transactions " "may cause unintended loss of privacy."; break; @@ -299,18 +307,22 @@ class DogecoinWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } } diff --git a/lib/wallets/wallet/impl/ecash_wallet.dart b/lib/wallets/wallet/impl/ecash_wallet.dart index a8741ba21..aacdc4f31 100644 --- a/lib/wallets/wallet/impl/ecash_wallet.dart +++ b/lib/wallets/wallet/impl/ecash_wallet.dart @@ -34,46 +34,37 @@ class EcashWallet extends Bip39HDWallet EcashWallet(CryptoCurrencyNetwork network) : super(Ecash(network) as T); @override - FilterOperation? get changeAddressFilterOperation => FilterGroup.and( - [ - ...standardChangeAddressFilters, - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/899", - ), - ), - ], - ); + FilterOperation? get changeAddressFilterOperation => FilterGroup.and([ + ...standardChangeAddressFilters, + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith(property: "value", value: "m/44'/899"), + ), + ]); @override - FilterOperation? get receivingAddressFilterOperation => FilterGroup.and( - [ - ...standardReceivingAddressFilters, - const ObjectFilter( - property: "derivationPath", - filter: FilterCondition.startsWith( - property: "value", - value: "m/44'/899", - ), - ), - ], - ); + FilterOperation? get receivingAddressFilterOperation => FilterGroup.and([ + ...standardReceivingAddressFilters, + const ObjectFilter( + property: "derivationPath", + filter: FilterCondition.startsWith(property: "value", value: "m/44'/899"), + ), + ]); // =========================================================================== @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .typeEqualTo(AddressType.nonWallet) - .and() - .not() - .subTypeEqualTo(AddressSubType.nonWallet) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .not() + .subTypeEqualTo(AddressSubType.nonWallet) + .findAll(); return allAddresses; } @@ -96,28 +87,32 @@ class EcashWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); final List> allTransactions = []; for (final txHash in allTxHashes) { - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -129,8 +124,9 @@ class EcashWallet extends Bip39HDWallet ); // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -288,7 +284,8 @@ class EcashWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -304,12 +301,8 @@ class EcashWallet extends Bip39HDWallet } @override - Future< - ({ - String? blockedReason, - bool blocked, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -321,8 +314,9 @@ class EcashWallet extends Bip39HDWallet if (scriptPubKeyHex != null) { // check for cash tokens try { - final ctOutput = - cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex); + final ctOutput = cash_tokens.unwrap_spk( + scriptPubKeyHex.toUint8ListFromHex, + ); if (ctOutput.token_data != null) { // found a token! blocked = true; @@ -350,19 +344,23 @@ class EcashWallet extends Bip39HDWallet // TODO: correct formula for ecash? @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: info.coin.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 10238cef6..215f41917 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -53,15 +53,17 @@ class EpiccashWallet extends Bip39Wallet { final int lastScannedBlock = info.epicData?.lastScannedBlock ?? 0; final _chainHeight = await chainHeight; final double restorePercent = lastScannedBlock / _chainHeight; - GlobalEventBus.instance - .fire(RefreshPercentChangedEvent(highestPercent, walletId)); + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercent, walletId), + ); if (restorePercent > highestPercent) { highestPercent = restorePercent; } final int blocksRemaining = _chainHeight - lastScannedBlock; - GlobalEventBus.instance - .fire(BlocksRemainingEvent(blocksRemaining, walletId)); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(blocksRemaining, walletId), + ); return restorePercent < 0 ? 0.0 : restorePercent; } @@ -84,17 +86,14 @@ class EpiccashWallet extends Bip39Wallet { Future cancelPendingTransactionAndPost(String txSlateId) async { try { _hackedCheckTorNodePrefs(); - final String wallet = (await secureStorageInterface.read( - key: '${walletId}_wallet', - ))!; + final String wallet = + (await secureStorageInterface.read(key: '${walletId}_wallet'))!; final result = await epiccash.LibEpiccash.cancelTransaction( wallet: wallet, transactionId: txSlateId, ); - Logging.instance.d( - "cancel $txSlateId result: $result", - ); + Logging.instance.d("cancel $txSlateId result: $result"); return result; } catch (e, s) { Logging.instance.e("", error: e, stackTrace: s); @@ -154,8 +153,10 @@ class EpiccashWallet extends Bip39Wallet { config["chain"] = "mainnet"; config["account"] = "default"; config["api_listen_port"] = port; - config["api_listen_interface"] = - nodeApiAddress.replaceFirst(uri.scheme, ""); + config["api_listen_interface"] = nodeApiAddress.replaceFirst( + uri.scheme, + "", + ); final String stringConfig = jsonEncode(config); return stringConfig; } @@ -219,12 +220,14 @@ class EpiccashWallet extends Bip39Wallet { } Future< - ({ - double awaitingFinalization, - double pending, - double spendable, - double total - })> _allWalletBalances() async { + ({ + double awaitingFinalization, + double pending, + double spendable, + double total, + }) + > + _allWalletBalances() async { _hackedCheckTorNodePrefs(); final wallet = await secureStorageInterface.read(key: '${walletId}_wallet'); const refreshFromNode = 0; @@ -243,9 +246,7 @@ class EpiccashWallet extends Bip39Wallet { try { final uri = Uri.parse('wss://$host:$port'); - channel = WebSocketChannel.connect( - uri, - ); + channel = WebSocketChannel.connect(uri); await channel.ready; @@ -280,9 +281,7 @@ class EpiccashWallet extends Bip39Wallet { "to": to, }; await info.updateExtraEpiccashWalletInfo( - epicData: info.epicData!.copyWith( - slatesToCommits: slatesToCommits, - ), + epicData: info.epicData!.copyWith(slatesToCommits: slatesToCommits), isar: mainDB.isar, ); return true; @@ -305,9 +304,7 @@ class EpiccashWallet extends Bip39Wallet { } /// Only index 0 is currently used in stack wallet. - Future
_generateAndStoreReceivingAddressForIndex( - int index, - ) async { + Future
_generateAndStoreReceivingAddressForIndex(int index) async { // Since only 0 is a valid index in stack wallet at this time, lets just // throw is not zero if (index != 0) { @@ -338,9 +335,7 @@ class EpiccashWallet extends Bip39Wallet { epicboxConfig: epicboxConfig.toString(), ); - Logging.instance.d( - "WALLET_ADDRESS_IS $walletAddress", - ); + Logging.instance.d("WALLET_ADDRESS_IS $walletAddress"); final address = Address( walletId: walletId, @@ -359,8 +354,9 @@ class EpiccashWallet extends Bip39Wallet { try { //First stop the current listener epiccash.LibEpiccash.stopEpicboxListener(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); // max number of blocks to scan per loop iteration const scanChunkSize = 10000; @@ -387,9 +383,7 @@ class EpiccashWallet extends Bip39Wallet { // update local cache await info.updateExtraEpiccashWalletInfo( - epicData: info.epicData!.copyWith( - lastScannedBlock: nextScannedBlock, - ), + epicData: info.epicData!.copyWith(lastScannedBlock: nextScannedBlock), isar: mainDB.isar, ); @@ -470,8 +464,9 @@ class EpiccashWallet extends Bip39Wallet { @override Future init({bool? isRestore}) async { if (isRestore != true) { - String? encodedWallet = - await secureStorageInterface.read(key: "${walletId}_wallet"); + String? encodedWallet = await secureStorageInterface.read( + key: "${walletId}_wallet", + ); // check if should create a new wallet if (encodedWallet == null) { @@ -543,8 +538,9 @@ class EpiccashWallet extends Bip39Wallet { ); final config = await _getRealConfig(); - final password = - await secureStorageInterface.read(key: '${walletId}_password'); + final password = await secureStorageInterface.read( + key: '${walletId}_password', + ); final walletOpen = await epiccash.LibEpiccash.openWallet( config: config, @@ -558,8 +554,11 @@ class EpiccashWallet extends Bip39Wallet { await updateNode(); } catch (e, s) { // do nothing, still allow user into wallet - Logging.instance - .w("$runtimeType init() failed: ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType init() failed: ", + error: e, + stackTrace: s, + ); } } } @@ -571,8 +570,9 @@ class EpiccashWallet extends Bip39Wallet { Future confirmSend({required TxData txData}) async { try { _hackedCheckTorNodePrefs(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig(); // TODO determine whether it is worth sending change to a change address. @@ -581,9 +581,7 @@ class EpiccashWallet extends Bip39Wallet { if (!receiverAddress.startsWith("http://") || !receiverAddress.startsWith("https://")) { - final bool isEpicboxConnected = await _testEpicboxServer( - epicboxConfig, - ); + final bool isEpicboxConnected = await _testEpicboxServer(epicboxConfig); if (!isEpicboxConnected) { throw Exception("Failed to send TX : Unable to reach epicbox server"); } @@ -618,9 +616,7 @@ class EpiccashWallet extends Bip39Wallet { txAddressInfo['to'] = txData.recipients!.first.address; await _putSendToAddresses(transaction, txAddressInfo); - return txData.copyWith( - txid: transaction.slateId, - ); + return txData.copyWith(txid: transaction.slateId); } catch (e, s) { Logging.instance.e("Epic cash confirmSend: ", error: e, stackTrace: s); rethrow; @@ -658,10 +654,7 @@ class EpiccashWallet extends Bip39Wallet { ); } - return txData.copyWith( - recipients: [recipient], - fee: feeAmount, - ); + return txData.copyWith(recipients: [recipient], fee: feeAmount); } catch (e, s) { Logging.instance.e("Epic cash prepareSend", error: e, stackTrace: s); rethrow; @@ -898,10 +891,7 @@ class EpiccashWallet extends Bip39Wallet { ), ); - await info.updateBalance( - newBalance: balance, - isar: mainDB.isar, - ); + await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "Epic cash wallet failed to update balance: ", @@ -915,20 +905,22 @@ class EpiccashWallet extends Bip39Wallet { Future updateTransactions() async { try { _hackedCheckTorNodePrefs(); - final wallet = - await secureStorageInterface.read(key: '${walletId}_wallet'); + final wallet = await secureStorageInterface.read( + key: '${walletId}_wallet', + ); const refreshFromNode = 1; - final myAddresses = await mainDB - .getAddresses(walletId) - .filter() - .typeEqualTo(AddressType.mimbleWimble) - .and() - .subTypeEqualTo(AddressSubType.receiving) - .and() - .valueIsNotEmpty() - .valueProperty() - .findAll(); + final myAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .typeEqualTo(AddressType.mimbleWimble) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .and() + .valueIsNotEmpty() + .valueProperty() + .findAll(); final myAddressesSet = myAddresses.toSet(); final transactions = await epiccash.LibEpiccash.getTransactions( @@ -943,7 +935,7 @@ class EpiccashWallet extends Bip39Wallet { for (final tx in transactions) { final isIncoming = tx.txType == epic_models.TransactionType.TxReceived || - tx.txType == epic_models.TransactionType.TxReceivedCancelled; + tx.txType == epic_models.TransactionType.TxReceivedCancelled; final slateId = tx.txSlateId; final commitId = slatesToCommits[slateId]?['commitId'] as String?; final numberOfMessages = tx.messages?.messages.length; @@ -964,9 +956,7 @@ class EpiccashWallet extends Bip39Wallet { OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: credit.toString(), - addresses: [ - if (addressFrom != null) addressFrom, - ], + addresses: [if (addressFrom != null) addressFrom], walletOwns: true, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -1010,11 +1000,12 @@ class EpiccashWallet extends Bip39Wallet { "onChainNote": onChainNote, "isCancelled": tx.txType == epic_models.TransactionType.TxSentCancelled || - tx.txType == epic_models.TransactionType.TxReceivedCancelled, - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + tx.txType == epic_models.TransactionType.TxReceivedCancelled, + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final txn = TransactionV2( @@ -1092,11 +1083,7 @@ class EpiccashWallet extends Bip39Wallet { ) != null; } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); return false; } } @@ -1105,8 +1092,9 @@ class EpiccashWallet extends Bip39Wallet { Future updateChainHeight() async { _hackedCheckTorNodePrefs(); final config = await _getRealConfig(); - final latestHeight = - await epiccash.LibEpiccash.getChainHeight(config: config); + final latestHeight = await epiccash.LibEpiccash.getChainHeight( + config: config, + ); await info.updateCachedChainHeight( newHeight: latestHeight, isar: mainDB.isar, @@ -1114,7 +1102,7 @@ class EpiccashWallet extends Bip39Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { _hackedCheckTorNodePrefs(); // setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function????? final int currentFee = await _nativeFee( @@ -1135,9 +1123,9 @@ class EpiccashWallet extends Bip39Wallet { numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, - fast: 1, - medium: 1, - slow: 1, + fast: BigInt.one, + medium: BigInt.one, + slow: BigInt.one, ); } @@ -1202,10 +1190,7 @@ Future deleteEpicWallet({ return "Tried to delete non existent epic wallet file with walletId=$walletId"; } else { try { - return epiccash.LibEpiccash.deleteWallet( - wallet: wallet, - config: config!, - ); + return epiccash.LibEpiccash.deleteWallet(wallet: wallet, config: config!); } catch (e, s) { Logging.instance.e("$e\n$s", error: e, stackTrace: s); return "deleteEpicWallet($walletId) failed..."; diff --git a/lib/wallets/wallet/impl/ethereum_wallet.dart b/lib/wallets/wallet/impl/ethereum_wallet.dart index e61da1df6..35e0bc174 100644 --- a/lib/wallets/wallet/impl/ethereum_wallet.dart +++ b/lib/wallets/wallet/impl/ethereum_wallet.dart @@ -5,8 +5,10 @@ import 'package:decimal/decimal.dart'; import 'package:ethereum_addresses/ethereum_addresses.dart'; import 'package:http/http.dart'; import 'package:isar/isar.dart'; +import 'package:web3dart/json_rpc.dart' show RPCError; import 'package:web3dart/web3dart.dart' as web3; +import '../../../dto/ethereum/eth_tx_dto.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; @@ -57,9 +59,10 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return web3.Web3Client(node.host, client); } - Amount estimateEthFee(int feeRate, int gasLimit, int decimals) { + Amount estimateEthFee(BigInt feeRate, int gasLimit, int decimals) { final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal()); - final fee = gasLimit.toDecimal() * + final fee = + gasLimit.toDecimal() * gweiAmount.toDecimal( scaleOnInfinitePrecision: cryptoCurrency.fractionDigits, ); @@ -95,9 +98,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == myAddress, ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -132,16 +133,15 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.none, otherData: jsonEncode(otherData), ); - return txData.copyWith( - tempTx: txn, - ); + return txData.copyWith(tempTx: txn); } // ==================== Overrides ============================================ @@ -151,11 +151,11 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { @override FilterOperation? get transactionFilterOperation => FilterGroup.not( - const FilterCondition.equalTo( - property: r"subType", - value: TransactionSubType.ethToken, - ), - ); + const FilterCondition.equalTo( + property: r"subType", + value: TransactionSubType.ethToken, + ), + ); @override FilterOperation? get changeAddressFilterOperation => @@ -189,7 +189,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { return estimateEthFee( feeRate, (cryptoCurrency as Ethereum).gasLimit, @@ -198,7 +198,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { } @override - Future get fees => EthereumAPI.getFees(); + Future get fees => EthereumAPI.getFees(); @override Future pingCheck() async { @@ -235,10 +235,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { fractionDigits: cryptoCurrency.fractionDigits, ), ); - await info.updateBalance( - newBalance: balance, - isar: mainDB.isar, - ); + await info.updateBalance(newBalance: balance, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "$runtimeType wallet failed to update balance: ", @@ -254,10 +251,7 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { final client = getEthClient(); final height = await client.getBlockNumber(); - await info.updateCachedChainHeight( - newHeight: height, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { Logging.instance.w( "$runtimeType Exception caught in chainHeight: ", @@ -279,7 +273,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { int firstBlock = 0; if (!isRescan) { - firstBlock = await mainDB.isar.transactionV2s + firstBlock = + await mainDB.isar.transactionV2s .where() .walletIdEqualTo(walletId) .heightProperty() @@ -300,8 +295,8 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { if (response.value == null) { Logging.instance.w( - "Failed to refresh transactions for ${cryptoCurrency.prettyName} ${info.name} " - "$walletId: ${response.exception}", + "Failed to refresh transactions for ${cryptoCurrency.prettyName}" + " ${info.name} $walletId: ${response.exception}", ); return; } @@ -311,108 +306,114 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return; } - final txsResponse = - await EthereumAPI.getEthTransactionNonces(response.value!); + web3.Web3Client? client; + final List allTxs = []; + for (final dto in response.value!) { + if (dto.nonce == null) { + client ??= getEthClient(); + final txInfo = await client.getTransactionByHash(dto.hash); + if (txInfo == null) { + // Something strange is happening + Logging.instance.w( + "Could not find transaction via RPC that was found use TrueBlocks " + "API.\nOffending tx: $dto", + ); + } else { + final updated = dto.copyWith(nonce: txInfo.nonce); + allTxs.add(updated); + } + } else { + allTxs.add(dto); + } + } - if (txsResponse.value != null) { - final allTxs = txsResponse.value!; - final List txns = []; - for (final tuple in allTxs) { - final element = tuple.item1; + final List txns = []; + for (final element in allTxs) { + if (element.hasToken && !element.isError) { + continue; + } - if (element.hasToken && !element.isError) { - continue; + //Calculate fees (GasLimit * gasPrice) + // int txFee = element.gasPrice * element.gasUsed; + final Amount txFee = element.gasCost; + final transactionAmount = element.value; + final addressFrom = checksumEthereumAddress(element.from); + final addressTo = checksumEthereumAddress(element.to); + + bool isIncoming; + bool txFailed = false; + if (addressFrom == thisAddress) { + if (element.isError) { + txFailed = true; } + isIncoming = false; + } else if (addressTo == thisAddress) { + isIncoming = true; + } else { + continue; + } - //Calculate fees (GasLimit * gasPrice) - // int txFee = element.gasPrice * element.gasUsed; - final Amount txFee = element.gasCost; - final transactionAmount = element.value; - final addressFrom = checksumEthereumAddress(element.from); - final addressTo = checksumEthereumAddress(element.to); - - bool isIncoming; - bool txFailed = false; - if (addressFrom == thisAddress) { - if (element.isError) { - txFailed = true; - } - isIncoming = false; - } else if (addressTo == thisAddress) { - isIncoming = true; - } else { - continue; - } + // hack eth tx data into inputs and outputs + final List outputs = []; + final List inputs = []; - // hack eth tx data into inputs and outputs - final List outputs = []; - final List inputs = []; - - final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( - scriptPubKeyHex: "00", - valueStringSats: transactionAmount.raw.toString(), - addresses: [ - addressTo, - ], - walletOwns: addressTo == thisAddress, - ); - final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: null, - scriptSigAsm: null, - sequence: null, - outpoint: null, - addresses: [addressFrom], - valueStringSats: transactionAmount.raw.toString(), - witness: null, - innerRedeemScriptAsm: null, - coinbase: null, - walletOwns: addressFrom == thisAddress, - ); + final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "00", + valueStringSats: transactionAmount.raw.toString(), + addresses: [addressTo], + walletOwns: addressTo == thisAddress, + ); + final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: null, + scriptSigAsm: null, + sequence: null, + outpoint: null, + addresses: [addressFrom], + valueStringSats: transactionAmount.raw.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: addressFrom == thisAddress, + ); - final TransactionType txType; - if (isIncoming) { - if (addressFrom == addressTo) { - txType = TransactionType.sentToSelf; - } else { - txType = TransactionType.incoming; - } + final TransactionType txType; + if (isIncoming) { + if (addressFrom == addressTo) { + txType = TransactionType.sentToSelf; } else { - txType = TransactionType.outgoing; + txType = TransactionType.incoming; } + } else { + txType = TransactionType.outgoing; + } - outputs.add(output); - inputs.add(input); - - final otherData = { - "nonce": tuple.item2, - "isCancelled": txFailed, - "overrideFee": txFee.toJsonString(), - }; - - final txn = TransactionV2( - walletId: walletId, - blockHash: element.blockHash, - hash: element.hash, - txid: element.hash, - timestamp: element.timestamp, - height: element.blockNumber, - inputs: List.unmodifiable(inputs), - outputs: List.unmodifiable(outputs), - version: -1, - type: txType, - subType: TransactionSubType.none, - otherData: jsonEncode(otherData), - ); + outputs.add(output); + inputs.add(input); - txns.add(txn); - } - await mainDB.updateOrPutTransactionV2s(txns); - } else { - Logging.instance.w( - "Failed to refresh transactions with nonces for ${cryptoCurrency.prettyName} " - "${info.name} $walletId: ${txsResponse.exception}", + final otherData = { + "nonce": element.nonce, + "isCancelled": txFailed, + "overrideFee": txFee.toJsonString(), + }; + + final txn = TransactionV2( + walletId: walletId, + blockHash: element.blockHash, + hash: element.hash, + txid: element.hash, + timestamp: element.timestamp, + height: element.blockNumber, + inputs: List.unmodifiable(inputs), + outputs: List.unmodifiable(outputs), + version: -1, + type: txType, + subType: TransactionSubType.none, + otherData: jsonEncode(otherData), ); + + txns.add(txn); } + await mainDB.updateOrPutTransactionV2s(txns); } @override @@ -421,88 +422,128 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { return false; } - @override - Future prepareSend({required TxData txData}) async { - final int - rate; // TODO: use BigInt for feeObject whenever FeeObject gets redone + Future getMyWeb3Address() async { + final myAddress = (await getCurrentReceivingAddress())!.value; + final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + return myWeb3Address; + } + + Future< + ({ + int nonce, + BigInt chainId, + BigInt baseFee, + BigInt maxBaseFee, + BigInt priorityFee, + }) + > + internalSharedPrepareSend({ + required TxData txData, + required web3.EthereumAddress myWeb3Address, + }) async { + if (txData.feeRateType == null) throw Exception("Missing fee rate type."); + if (txData.feeRateType == FeeRateType.custom && + txData.ethEIP1559Fee == null) { + throw Exception("Missing custom EIP-1559 values."); + } + + await updateBalance(); + + final client = getEthClient(); + final chainId = await client.getChainId(); + final nonce = + txData.nonce ?? + await client.getTransactionCount( + myWeb3Address, + atBlock: const web3.BlockNum.pending(), + ); + final feeObject = await fees; + final baseFee = feeObject.suggestBaseFee; + BigInt maxBaseFee = baseFee; + BigInt priorityFee; + switch (txData.feeRateType!) { case FeeRateType.fast: - rate = feeObject.fast; + priorityFee = feeObject.fast - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.average: - rate = feeObject.medium; + priorityFee = feeObject.medium - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.slow: - rate = feeObject.slow; + priorityFee = feeObject.slow - baseFee; + if (priorityFee.isNegative) priorityFee = BigInt.zero; break; + case FeeRateType.custom: - throw UnimplementedError("custom eth fees"); + priorityFee = txData.ethEIP1559Fee!.priorityFeeWei; + maxBaseFee = txData.ethEIP1559Fee!.maxBaseFeeWei; + break; } - final feeEstimate = await estimateFeeFor(Amount.zero, rate); - - // bool isSendAll = false; - // final availableBalance = balance.spendable; - // if (satoshiAmount == availableBalance) { - // isSendAll = true; - // } - // - // if (isSendAll) { - // //Subtract fee amount from send amount - // satoshiAmount -= feeEstimate; - // } - - final client = getEthClient(); + if (baseFee > maxBaseFee) { + throw Exception("Base cannot be greater than max base fee"); + } + if (priorityFee > maxBaseFee) { + throw Exception("Priority fee cannot be greater than max base fee"); + } - final myAddress = (await getCurrentReceivingAddress())!.value; - final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + return ( + nonce: nonce, + chainId: chainId, + baseFee: baseFee, + maxBaseFee: maxBaseFee, + priorityFee: priorityFee, + ); + } + @override + Future prepareSend({required TxData txData}) async { final amount = txData.recipients!.first.amount; final address = txData.recipients!.first.address; - // final est = await client.estimateGas( - // sender: myWeb3Address, - // to: web3.EthereumAddress.fromHex(address), - // gasPrice: web3.EtherAmount.fromUnitAndValue( - // web3.EtherUnit.wei, - // rate, - // ), - // amountOfGas: BigInt.from((cryptoCurrency as Ethereum).gasLimit), - // value: web3.EtherAmount.inWei(amount.raw), - // ); - - final nonce = txData.nonce ?? - await client.getTransactionCount( - myWeb3Address, - atBlock: const web3.BlockNum.pending(), - ); + final myWeb3Address = await getMyWeb3Address(); + + final prep = await internalSharedPrepareSend( + txData: txData, + myWeb3Address: myWeb3Address, + ); - // final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); - // print("=============================================================="); - // print("ETH client.estimateGas: $est"); - // print("ETH estimateFeeFor : $feeEstimate"); - // print("ETH nonce custom response: $nResponse"); - // print("ETH actual nonce : $nonce"); - // print("=============================================================="); + // double check balance after internalSharedPrepareSend call to ensure + // balance is up to date + if (amount > info.cachedBalance.spendable) { + throw Exception("Insufficient balance"); + } final tx = web3.Transaction( to: web3.EthereumAddress.fromHex(address), - gasPrice: web3.EtherAmount.fromUnitAndValue( + maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumMinGasLimit, + value: web3.EtherAmount.inWei(amount.raw), + nonce: prep.nonce, + maxFeePerGas: web3.EtherAmount.fromBigInt( web3.EtherUnit.wei, - rate, + prep.maxBaseFee, ), - maxGas: (cryptoCurrency as Ethereum).gasLimit, - value: web3.EtherAmount.inWei(amount.raw), - nonce: nonce, + maxPriorityFeePerGas: web3.EtherAmount.fromBigInt( + web3.EtherUnit.wei, + prep.priorityFee, + ), + ); + + final feeEstimate = await estimateFeeFor( + Amount.zero, + prep.maxBaseFee + prep.priorityFee, ); return txData.copyWith( nonce: tx.nonce, web3dartTransaction: tx, fee: feeEstimate, - feeInWei: BigInt.from(rate), - chainId: (await client.getChainId()), + chainId: prep.chainId, ); } @@ -516,21 +557,24 @@ class EthereumWallet extends Bip39Wallet with PrivateKeyInterface { await _initCredentials(); } - final txid = await client.sendTransaction( - _credentials!, - txData.web3dartTransaction!, - chainId: txData.chainId!.toInt(), - ); + try { + final txid = await client.sendTransaction( + _credentials!, + txData.web3dartTransaction!, + chainId: txData.chainId!.toInt(), + ); - final data = (prepareTempTx ?? _prepareTempTx)( - txData.copyWith( - txid: txid, - txHash: txid, - ), - (await getCurrentReceivingAddress())!.value, - ); + final data = (prepareTempTx ?? _prepareTempTx)( + txData.copyWith(txid: txid, txHash: txid), + (await getCurrentReceivingAddress())!.value, + ); - return await updateSentCachedTxData(txData: data); + return await updateSentCachedTxData(txData: data); + } on RPCError catch (e) { + final message = + "${e.toString()}${e.data == null ? "" : e.data.toString()}"; + throw Exception(message); + } } @override diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index aed72a33a..75d7290a9 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -60,9 +60,7 @@ class FiroWallet extends Bip39HDWallet if (txData.tempTx != null) { await mainDB.updateOrPutTransactionV2s([txData.tempTx!]); _unconfirmedTxids.add(txData.tempTx!.txid); - Logging.instance.d( - "Added firo unconfirmed: ${txData.tempTx!.txid}", - ); + Logging.instance.d("Added firo unconfirmed: ${txData.tempTx!.txid}"); } return txData; } @@ -72,36 +70,41 @@ class FiroWallet extends Bip39HDWallet final List
allAddressesOld = await fetchAddressesForElectrumXScan(); - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => convertAddressString(e.value)) + .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => convertAddressString(e.value)) - .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => convertAddressString(e.value)) + .toSet(); final allAddressesSet = {...receivingAddresses, ...changeAddresses}; - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); - final sparkCoins = await mainDB.isar.sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .findAll(); + final sparkCoins = + await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .findAll(); final List> allTransactions = []; // some lelantus transactions aren't fetched via wallet addresses so they // will never show as confirmed in the gui. - final unconfirmedTransactions = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightIsNull() - .findAll(); + final unconfirmedTransactions = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .findAll(); for (final tx in unconfirmedTransactions) { final txn = await electrumXCachedClient.getTransaction( txHash: tx.txid, @@ -113,10 +116,7 @@ class FiroWallet extends Bip39HDWallet if (height != null) { // tx was mined // add to allTxHashes - final info = { - "tx_hash": tx.txid, - "height": height, - }; + final info = {"tx_hash": tx.txid, "height": height}; allTxHashes.add(info); } } @@ -126,10 +126,7 @@ class FiroWallet extends Bip39HDWallet sparkTxids.add(coin.txHash); // check for duplicates before adding to list if (allTxHashes.indexWhere((e) => e["tx_hash"] == coin.txHash) == -1) { - final info = { - "tx_hash": coin.txHash, - "height": coin.height, - }; + final info = {"tx_hash": coin.txHash, "height": coin.height}; allTxHashes.add(info); } } @@ -138,9 +135,7 @@ class FiroWallet extends Bip39HDWallet for (final txid in missing.map((e) => e.txid).toSet()) { // check for duplicates before adding to list if (allTxHashes.indexWhere((e) => e["tx_hash"] == txid) == -1) { - final info = { - "tx_hash": txid, - }; + final info = {"tx_hash": txid}; allTxHashes.add(info); } } @@ -148,12 +143,13 @@ class FiroWallet extends Bip39HDWallet final currentHeight = await chainHeight; for (final txHash in allTxHashes) { - final storedTx = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txHash["tx_hash"] as String) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txHash["tx_hash"] as String) + .findFirst(); if (storedTx?.isConfirmed( currentHeight, @@ -180,8 +176,9 @@ class FiroWallet extends Bip39HDWallet } // check for duplicates before adding to list - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] ??= txHash["height"]; allTransactions.add(tx); @@ -290,17 +287,19 @@ class FiroWallet extends Bip39HDWallet if (output.addresses.isEmpty && output.scriptPubKeyHex.length >= 488) { // likely spark related - final opByte = output.scriptPubKeyHex - .substring(0, 2) - .toUint8ListFromHex - .first; + final opByte = + output.scriptPubKeyHex + .substring(0, 2) + .toUint8ListFromHex + .first; if (opByte == OP_SPARKMINT || opByte == OP_SPARKSMINT) { final serCoin = base64Encode( output.scriptPubKeyHex.substring(2, 488).toUint8ListFromHex, ); - final coin = sparkCoinsInvolvedReceived - .where((e) => e.serializedCoinB64!.startsWith(serCoin)) - .firstOrNull; + final coin = + sparkCoinsInvolvedReceived + .where((e) => e.serializedCoinB64!.startsWith(serCoin)) + .firstOrNull; if (coin == null) { // not ours @@ -308,9 +307,7 @@ class FiroWallet extends Bip39HDWallet output = output.copyWith( walletOwns: true, valueStringSats: coin.value.toString(), - addresses: [ - coin.address, - ], + addresses: [coin.address], ); } } @@ -395,11 +392,10 @@ class FiroWallet extends Bip39HDWallet txid: txData["txid"] as String, network: cryptoCurrency.network, ); - spentSparkCoins = sparkCoinsInvolvedSpent - .where( - (e) => tags.contains(e.lTagHash), - ) - .toList(); + spentSparkCoins = + sparkCoinsInvolvedSpent + .where((e) => tags.contains(e.lTagHash)) + .toList(); } else if (isSparkSpend) { parseAnonFees(); } else if (isSparkMint) { @@ -483,10 +479,11 @@ class FiroWallet extends Bip39HDWallet if (usedCoins.isNotEmpty) { input = input.copyWith( addresses: usedCoins.map((e) => e.address).toList(), - valueStringSats: usedCoins - .map((e) => e.value) - .reduce((value, element) => value += element) - .toString(), + valueStringSats: + usedCoins + .map((e) => e.value) + .reduce((value, element) => value += element) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -497,10 +494,11 @@ class FiroWallet extends Bip39HDWallet spentSparkCoins.isNotEmpty) { input = input.copyWith( addresses: spentSparkCoins.map((e) => e.address).toList(), - valueStringSats: spentSparkCoins - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + e) - .toString(), + valueStringSats: + spentSparkCoins + .map((e) => e.value) + .fold(BigInt.zero, (p, e) => p + e) + .toString(), walletOwns: true, ); wasSentFromThisWallet = true; @@ -570,11 +568,7 @@ class FiroWallet extends Bip39HDWallet String? otherData; if (anonFees != null) { - otherData = jsonEncode( - { - "overrideFee": anonFees!.toJsonString(), - }, - ); + otherData = jsonEncode({"overrideFee": anonFees!.toJsonString()}); } final tx = TransactionV2( @@ -584,7 +578,8 @@ class FiroWallet extends Bip39HDWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -613,12 +608,8 @@ class FiroWallet extends Bip39HDWallet } @override - Future< - ({ - String? blockedReason, - bool blocked, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map? jsonTX, @@ -631,7 +622,8 @@ class FiroWallet extends Bip39HDWallet if (jsonUTXO["value"] is int) { // TODO: [prio=high] use special electrumx call to verify the 1000 Firo output is masternode // electrumx call should exist now. Unsure if it works though - blocked = Amount.fromDecimal( + blocked = + Amount.fromDecimal( Decimal.fromInt( 1000, // 1000 firo output is a possible master node ), @@ -654,7 +646,8 @@ class FiroWallet extends Bip39HDWallet } if (blocked) { - blockedReason = "Possible masternode collateral. " + blockedReason = + "Possible masternode collateral. " "Unlock and spend at your own risk."; label = "Possible masternode collateral"; } @@ -707,13 +700,11 @@ class FiroWallet extends Bip39HDWallet final List> lelantusFutures = []; final enableLelantusScanning = info.otherData[WalletInfoKeys.enableLelantusScanning] as bool? ?? - false; + false; if (enableLelantusScanning) { latestSetId = await electrumXClient.getLelantusLatestCoinId(); lelantusFutures.add( - electrumXCachedClient.getUsedCoinSerials( - cryptoCurrency: info.coin, - ), + electrumXCachedClient.getUsedCoinSerials(cryptoCurrency: info.coin), ); lelantusFutures.add(getSetDataMap(latestSetId)); } @@ -733,9 +724,9 @@ class FiroWallet extends Bip39HDWallet } final sparkUsedCoinTagsFuture = FiroCacheCoordinator.runFetchAndUpdateSparkUsedCoinTags( - electrumXClient, - cryptoCurrency.network, - ); + electrumXClient, + cryptoCurrency.network, + ); // receiving addresses Logging.instance.i("checking receiving addresses..."); @@ -745,17 +736,8 @@ class FiroWallet extends Bip39HDWallet for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) - : checkGapsLinearly( - root, - type, - receiveChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), ); } @@ -764,17 +746,8 @@ class FiroWallet extends Bip39HDWallet for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) - : checkGapsLinearly( - root, - type, - changeChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), ); } @@ -834,10 +807,7 @@ class FiroWallet extends Bip39HDWallet await mainDB.updateOrPutAddresses(addressesToStore); - await Future.wait([ - updateTransactions(), - updateUTXOs(), - ]); + await Future.wait([updateTransactions(), updateUTXOs()]); final List> futures = []; if (enableLelantusScanning) { @@ -865,9 +835,7 @@ class FiroWallet extends Bip39HDWallet usedSerialNumbers: usedSerialsSet!, setDataMap: setDataMap!, ), - recoverSparkWallet( - latestSparkCoinId: latestSparkCoinId, - ), + recoverSparkWallet(latestSparkCoinId: latestSparkCoinId), ]); } else { if (enableLelantusScanning) { @@ -877,9 +845,7 @@ class FiroWallet extends Bip39HDWallet setDataMap: setDataMap!, ); } - await recoverSparkWallet( - latestSparkCoinId: latestSparkCoinId, - ); + await recoverSparkWallet(latestSparkCoinId: latestSparkCoinId); } }); @@ -900,19 +866,23 @@ class FiroWallet extends Bip39HDWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // =========================================================================== diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index 034056817..e72b8e633 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -44,17 +44,18 @@ class LitecoinWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } @@ -67,14 +68,16 @@ class LitecoinWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; @@ -84,17 +87,19 @@ class LitecoinWallet ); // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -107,8 +112,9 @@ class LitecoinWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -248,12 +254,13 @@ class LitecoinWallet // Check for special Litecoin outputs like ordinals. if (outputs.isNotEmpty) { // may not catch every case but it is much quicker - final hasOrdinal = await mainDB.isar.ordinals - .where() - .filter() - .walletIdEqualTo(walletId) - .utxoTXIDEqualTo(txData["txid"] as String) - .isNotEmpty(); + final hasOrdinal = + await mainDB.isar.ordinals + .where() + .filter() + .walletIdEqualTo(walletId) + .utxoTXIDEqualTo(txData["txid"] as String) + .isNotEmpty(); if (hasOrdinal) { subType = TransactionSubType.ordinal; } @@ -307,7 +314,8 @@ class LitecoinWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -323,79 +331,84 @@ class LitecoinWallet } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } -// -// @override -// Future coinSelection({required TxData txData}) async { -// final isCoinControl = txData.utxos != null; -// final isSendAll = txData.amount == info.cachedBalance.spendable; -// -// final utxos = -// txData.utxos?.toList() ?? await mainDB.getUTXOs(walletId).findAll(); -// -// final currentChainHeight = await chainHeight; -// final List spendableOutputs = []; -// int spendableSatoshiValue = 0; -// -// // Build list of spendable outputs and totaling their satoshi amount -// for (final utxo in utxos) { -// if (utxo.isBlocked == false && -// utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && -// utxo.used != true) { -// spendableOutputs.add(utxo); -// spendableSatoshiValue += utxo.value; -// } -// } -// -// if (isCoinControl && spendableOutputs.length < utxos.length) { -// throw ArgumentError("Attempted to use an unavailable utxo"); -// } -// -// if (spendableSatoshiValue < txData.amount!.raw.toInt()) { -// throw Exception("Insufficient balance"); -// } else if (spendableSatoshiValue == txData.amount!.raw.toInt() && -// !isSendAll) { -// throw Exception("Insufficient balance to pay transaction fee"); -// } -// -// if (isCoinControl) { -// } else { -// final selection = cs.coinSelection( -// spendableOutputs -// .map((e) => cs.InputModel( -// i: e.vout, -// txid: e.txid, -// value: e.value, -// address: e.address, -// )) -// .toList(), -// txData.recipients! -// .map((e) => cs.OutputModel( -// address: e.address, -// value: e.amount.raw.toInt(), -// )) -// .toList(), -// txData.feeRateAmount!, -// 10, // TODO: ??????????????????????????????? -// ); -// -// // .inputs and .outputs will be null if no solution was found -// if (selection.inputs!.isEmpty || selection.outputs!.isEmpty) { -// throw Exception("coin selection failed"); -// } -// } -// } + + // + // @override + // Future coinSelection({required TxData txData}) async { + // final isCoinControl = txData.utxos != null; + // final isSendAll = txData.amount == info.cachedBalance.spendable; + // + // final utxos = + // txData.utxos?.toList() ?? await mainDB.getUTXOs(walletId).findAll(); + // + // final currentChainHeight = await chainHeight; + // final List spendableOutputs = []; + // int spendableSatoshiValue = 0; + // + // // Build list of spendable outputs and totaling their satoshi amount + // for (final utxo in utxos) { + // if (utxo.isBlocked == false && + // utxo.isConfirmed(currentChainHeight, cryptoCurrency.minConfirms) && + // utxo.used != true) { + // spendableOutputs.add(utxo); + // spendableSatoshiValue += utxo.value; + // } + // } + // + // if (isCoinControl && spendableOutputs.length < utxos.length) { + // throw ArgumentError("Attempted to use an unavailable utxo"); + // } + // + // if (spendableSatoshiValue < txData.amount!.raw.toInt()) { + // throw Exception("Insufficient balance"); + // } else if (spendableSatoshiValue == txData.amount!.raw.toInt() && + // !isSendAll) { + // throw Exception("Insufficient balance to pay transaction fee"); + // } + // + // if (isCoinControl) { + // } else { + // final selection = cs.coinSelection( + // spendableOutputs + // .map((e) => cs.InputModel( + // i: e.vout, + // txid: e.txid, + // value: e.value, + // address: e.address, + // )) + // .toList(), + // txData.recipients! + // .map((e) => cs.OutputModel( + // address: e.address, + // value: e.amount.raw.toInt(), + // )) + // .toList(), + // txData.feeRateAmount!, + // 10, // TODO: ??????????????????????????????? + // ); + // + // // .inputs and .outputs will be null if no solution was found + // if (selection.inputs!.isEmpty || selection.outputs!.isEmpty) { + // throw Exception("coin selection failed"); + // } + // } + // } } diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index a99cf30a3..03bbfd5c3 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -12,14 +12,14 @@ class MoneroWallet extends LibMoneroWallet { : super(Monero(network), lib_monero_compat.WalletType.monero); @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); } lib_monero.TransactionPriority priority; - switch (feeRate) { + switch (feeRate.toInt()) { case 1: priority = lib_monero.TransactionPriority.low; break; diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index bf6f43e41..bb3f020f3 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -200,17 +200,21 @@ class NamecoinWallet } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // TODO: Check if this is the correct formula for namecoin. @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -897,7 +901,7 @@ class NamecoinWallet if (customSatsPerVByte != null) { final result = await coinSelectionName( - txData: txData.copyWith(feeRateAmount: -1), + txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), utxos: utxos?.toList(), coinControl: coinControl, ); @@ -911,10 +915,10 @@ class NamecoinWallet } return result; - } else if (feeRateType is FeeRateType || feeRateAmount is int) { - late final int rate; + } else if (feeRateType is FeeRateType || feeRateAmount is BigInt) { + late final BigInt rate; if (feeRateType is FeeRateType) { - int fee = 0; + BigInt fee = BigInt.zero; final feeObject = await fees; switch (feeRateType) { case FeeRateType.fast: @@ -931,7 +935,7 @@ class NamecoinWallet } rate = fee; } else { - rate = feeRateAmount as int; + rate = feeRateAmount!; } final result = await coinSelectionName( diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 80db6e2ef..1188625cb 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -45,29 +45,26 @@ class ParticlWallet @override Future> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } -// =========================================================================== + // =========================================================================== @override - Future< - ({ - bool blocked, - String? blockedReason, - String? utxoLabel, - })> checkBlockUTXO( + Future<({bool blocked, String? blockedReason, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, @@ -98,8 +95,9 @@ class ParticlWallet utxoLabel = "Unsupported output type."; } else if (output['scriptPubKey'] != null) { if (output['scriptPubKey']?['asm'] is String && - (output['scriptPubKey']['asm'] as String) - .contains("OP_ISCOINSTAKE")) { + (output['scriptPubKey']['asm'] as String).contains( + "OP_ISCOINSTAKE", + )) { blocked = true; blockedReason = "Spending staking"; utxoLabel = "Unsupported output type."; @@ -111,21 +109,25 @@ class ParticlWallet return ( blocked: blocked, blockedReason: blockedReason, - utxoLabel: utxoLabel + utxoLabel: utxoLabel, ); } @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate( + int inputCount, + int outputCount, + BigInt feeRatePerKB, + ) { return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -138,30 +140,34 @@ class ParticlWallet await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List> allTxHashes = - await fetchHistory(allAddressesSet); + final List> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -174,8 +180,9 @@ class ParticlWallet ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -325,7 +332,8 @@ class ParticlWallet txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -372,32 +380,31 @@ class ParticlWallet switch (sd.derivePathType) { case DerivePathType.bip44: - data = bitcoindart - .P2PKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2PKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip49: - final p2wpkh = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + final p2wpkh = + bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; redeem = p2wpkh.output; - data = bitcoindart - .P2SH( - data: bitcoindart.PaymentData(redeem: p2wpkh), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip84: @@ -405,14 +412,13 @@ class ParticlWallet // prevOut: coinlib.OutPoint.fromHex(sd.utxo.txid, sd.utxo.vout), // publicKey: keys.publicKey, // ); - data = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -432,9 +438,7 @@ class ParticlWallet extraData.add((output: output, redeem: redeem)); } - final txb = bitcoindart.TransactionBuilder( - network: convertedNetwork, - ); + final txb = bitcoindart.TransactionBuilder(network: convertedNetwork); const version = 160; // buildTransaction overridden for Particl to set this. // TODO: [prio=low] refactor overridden buildTransaction to use eg. cryptocurrency.networkParams.txVersion. txb.setVersion(version); @@ -463,9 +467,10 @@ class ParticlWallet txid: utxoSigningData[i].utxo.txid, vout: utxoSigningData[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], + addresses: + utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], valueStringSats: utxoSigningData[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, @@ -487,10 +492,9 @@ class ParticlWallet OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -518,8 +522,11 @@ class ParticlWallet ); } } catch (e, s) { - Logging.instance.e("Caught exception while signing transaction: ", - error: e, stackTrace: s); + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); rethrow; } diff --git a/lib/wallets/wallet/impl/peercoin_wallet.dart b/lib/wallets/wallet/impl/peercoin_wallet.dart index 153131415..09c69e2cc 100644 --- a/lib/wallets/wallet/impl/peercoin_wallet.dart +++ b/lib/wallets/wallet/impl/peercoin_wallet.dart @@ -54,13 +54,13 @@ class PeercoinWallet // =========================================================================== @override - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB) { // TODO: actually do this properly for peercoin // this is probably wrong for peercoin return Amount( rawValue: BigInt.from( ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(), + (feeRatePerKB.toInt() / 1000).ceil(), ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -68,8 +68,8 @@ class PeercoinWallet /// we can just pretend vSize is size for peercoin @override - int estimateTxFee({required int vSize, required int feeRatePerKB}) { - return vSize * (feeRatePerKB / 1000).ceil(); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}) { + return vSize * (feeRatePerKB.toInt() / 1000).ceil(); } // =========================================================================== diff --git a/lib/wallets/wallet/impl/solana_wallet.dart b/lib/wallets/wallet/impl/solana_wallet.dart index 8ae0db348..0b7c2911d 100644 --- a/lib/wallets/wallet/impl/solana_wallet.dart +++ b/lib/wallets/wallet/impl/solana_wallet.dart @@ -55,13 +55,13 @@ class SolanaWallet extends Bip39Wallet { return addressStruct; } - Future _getCurrentBalanceInLamports() async { + Future _getCurrentBalanceInLamports() async { _checkClient(); final balance = await _rpcClient?.getBalance((await _getKeyPair()).address); - return balance!.value; + return BigInt.from(balance!.value); } - Future _getEstimatedNetworkFee(Amount transferAmount) async { + Future _getEstimatedNetworkFee(Amount transferAmount) async { _checkClient(); final latestBlockhash = await _rpcClient?.getLatestBlockhash(); final pubKey = (await _getKeyPair()).publicKey; @@ -79,9 +79,13 @@ class SolanaWallet extends Bip39Wallet { feePayer: pubKey, ); - return await _rpcClient?.getFeeForMessage( + final estimate = await _rpcClient?.getFeeForMessage( base64Encode(compiledMessage.toByteArray().toList()), ); + + if (estimate == null) return null; + + return BigInt.from(estimate); } @override @@ -99,7 +103,11 @@ class SolanaWallet extends Bip39Wallet { await mainDB.updateOrPutAddresses([address]); } } catch (e, s) { - Logging.instance.e("$runtimeType checkSaveInitialReceivingAddress() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -133,28 +141,33 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Account does not appear to exist"); } - final int minimumRent = - await _rpcClient!.getMinimumBalanceForRentExemption( - accInfo.value!.data.toString().length, + final BigInt minimumRent = BigInt.from( + await _rpcClient!.getMinimumBalanceForRentExemption( + accInfo.value!.data.toString().length, + ), ); if (minimumRent > ((await _getCurrentBalanceInLamports()) - - txData.amount!.raw.toInt() - + txData.amount!.raw - feeAmount)) { throw Exception( "Insufficient remaining balance for rent exemption, minimum rent: " - "${minimumRent / pow(10, cryptoCurrency.fractionDigits)}", + "${minimumRent.toInt() / pow(10, cryptoCurrency.fractionDigits)}", ); } return txData.copyWith( fee: Amount( - rawValue: BigInt.from(feeAmount), + rawValue: feeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), ); } catch (e, s) { - Logging.instance.e("$runtimeType Solana prepareSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Solana prepareSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -166,8 +179,9 @@ class SolanaWallet extends Bip39Wallet { final keyPair = await _getKeyPair(); final recipientAccount = txData.recipients!.first; - final recipientPubKey = - Ed25519HDPublicKey.fromBase58(recipientAccount.address); + final recipientPubKey = Ed25519HDPublicKey.fromBase58( + recipientAccount.address, + ); final message = Message( instructions: [ SystemInstruction.transfer( @@ -187,17 +201,19 @@ class SolanaWallet extends Bip39Wallet { ); final txid = await _rpcClient?.signAndSendTransaction(message, [keyPair]); - return txData.copyWith( - txid: txid, - ); + return txData.copyWith(txid: txid); } catch (e, s) { - Logging.instance.e("$runtimeType Solana confirmSend failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType Solana confirmSend failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { _checkClient(); if (info.cachedBalance.spendable.raw == BigInt.zero) { @@ -212,10 +228,7 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Failed to get fees, please check your node connection."); } - return Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ); + return Amount(rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits); } @override @@ -250,7 +263,9 @@ class SolanaWallet extends Bip39Wallet { health = await _rpcClient?.getHealth(); return health != null; } catch (e, s) { - Logging.instance.e("$runtimeType Solana pingCheck failed \"health response=$health\": $e\n$s"); + Logging.instance.e( + "$runtimeType Solana pingCheck failed \"health response=$health\": $e\n$s", + ); return Future.value(false); } } @@ -295,10 +310,10 @@ class SolanaWallet extends Bip39Wallet { throw Exception("Account does not appear to exist"); } - final int minimumRent = - await _rpcClient!.getMinimumBalanceForRentExemption( - accInfo.value!.data.toString().length, - ); + final int minimumRent = await _rpcClient! + .getMinimumBalanceForRentExemption( + accInfo.value!.data.toString().length, + ); final spendableBalance = balance!.value - minimumRent; final newBalance = Balance( @@ -322,7 +337,11 @@ class SolanaWallet extends Bip39Wallet { await info.updateBalance(newBalance: newBalance, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("Error getting balance in solana_wallet.dart: ", error: e, stackTrace: s); + Logging.instance.e( + "Error getting balance in solana_wallet.dart: ", + error: e, + stackTrace: s, + ); } } @@ -339,23 +358,29 @@ class SolanaWallet extends Bip39Wallet { isar: mainDB.isar, ); } catch (e, s) { - Logging.instance.e("Error occurred in solana_wallet.dart while getting" - " chain height for solana: $e\n$s"); + Logging.instance.e( + "Error occurred in solana_wallet.dart while getting" + " chain height for solana: $e\n$s", + ); } } @override Future updateNode() async { - _solNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? + _solNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? info.coin.defaultNode; await refresh(); } @override NodeModel getCurrentNode() { - _solNode ??= NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? + _solNode ??= + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? info.coin.defaultNode; return _solNode!; @@ -370,8 +395,9 @@ class SolanaWallet extends Bip39Wallet { (await _getKeyPair()).publicKey, encoding: Encoding.jsonParsed, ); - final txsList = - List>.empty(growable: true); + final txsList = List>.empty( + growable: true, + ); final myAddress = (await getCurrentReceivingAddress())!; @@ -384,8 +410,9 @@ class SolanaWallet extends Bip39Wallet { (tx.transaction as ParsedTransaction).message.accountKeys[1].pubkey; var txType = isar.TransactionType.unknown; final txAmount = Amount( - rawValue: - BigInt.from(tx.meta!.postBalances[1] - tx.meta!.preBalances[1]), + rawValue: BigInt.from( + tx.meta!.postBalances[1] - tx.meta!.preBalances[1], + ), fractionDigits: cryptoCurrency.fractionDigits, ); @@ -429,9 +456,10 @@ class SolanaWallet extends Bip39Wallet { derivationIndex: 0, derivationPath: DerivationPath()..value = _addressDerivationPath, type: AddressType.solana, - subType: txType == isar.TransactionType.outgoing - ? AddressSubType.unknown - : AddressSubType.receiving, + subType: + txType == isar.TransactionType.outgoing + ? AddressSubType.unknown + : AddressSubType.receiving, ); txsList.add(Tuple2(transaction, txAddress)); @@ -440,8 +468,10 @@ class SolanaWallet extends Bip39Wallet { } on NodeTorMismatchConfigException { rethrow; } catch (e, s) { - Logging.instance.e("Error occurred in solana_wallet.dart while getting" - " transactions for solana: $e\n$s"); + Logging.instance.e( + "Error occurred in solana_wallet.dart while getting" + " transactions for solana: $e\n$s", + ); } } diff --git a/lib/wallets/wallet/impl/stellar_wallet.dart b/lib/wallets/wallet/impl/stellar_wallet.dart index 811858546..91af9758f 100644 --- a/lib/wallets/wallet/impl/stellar_wallet.dart +++ b/lib/wallets/wallet/impl/stellar_wallet.dart @@ -33,34 +33,34 @@ class StellarWallet extends Bip39Wallet { final bus = GlobalEventBus.instance; // Listen for tor status changes. - _torStatusListener = bus.on().listen( - (event) async { - switch (event.newStatus) { - case TorConnectionStatus.connecting: - if (!_torConnectingLock.isLocked) { - await _torConnectingLock.acquire(); - } - _requireMutex = true; - break; + _torStatusListener = bus.on().listen(( + event, + ) async { + switch (event.newStatus) { + case TorConnectionStatus.connecting: + if (!_torConnectingLock.isLocked) { + await _torConnectingLock.acquire(); + } + _requireMutex = true; + break; - case TorConnectionStatus.connected: - case TorConnectionStatus.disconnected: - if (_torConnectingLock.isLocked) { - _torConnectingLock.release(); - } - _requireMutex = false; - 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 { - _stellarSdk?.httpClient.close(); - _stellarSdk = null; - }, - ); + _torPreferenceListener = bus.on().listen(( + event, + ) async { + _stellarSdk?.httpClient.close(); + _stellarSdk = null; + }); } void _hackedCheck() { @@ -116,12 +116,10 @@ class StellarWallet extends Bip39Wallet { // ============== Private ==================================================== // add finalizer to cancel stream subscription when all references to an // instance of this becomes inaccessible - final _ = Finalizer( - (p0) { - p0._torPreferenceListener?.cancel(); - p0._torStatusListener?.cancel(); - }, - ); + final _ = Finalizer((p0) { + p0._torPreferenceListener?.cancel(); + p0._torStatusListener?.cancel(); + }); StreamSubscription? _torStatusListener; StreamSubscription? _torPreferenceListener; @@ -131,9 +129,9 @@ class StellarWallet extends Bip39Wallet { stellar.StellarSDK? _stellarSdk; - Future _getBaseFee() async { + Future _getBaseFee() async { final fees = await (await stellarSdk).feeStats.execute(); - return int.parse(fees.lastLedgerBaseFee); + return BigInt.parse(fees.lastLedgerBaseFee); } stellar.StellarSDK _getFreshSdk() { @@ -145,15 +143,9 @@ class StellarWallet extends Bip39Wallet { TorService.sharedInstance.getProxyInfo(); _httpClient = HttpClient(); - SocksTCPClient.assignToHttpClient( - _httpClient, - [ - ProxySettings( - proxyInfo.host, - proxyInfo.port, - ), - ], - ); + SocksTCPClient.assignToHttpClient(_httpClient, [ + ProxySettings(proxyInfo.host, proxyInfo.port), + ]); } return stellar.StellarSDK( @@ -166,16 +158,18 @@ class StellarWallet extends Bip39Wallet { bool exists = false; try { - final receiverAccount = - await (await stellarSdk).accounts.account(accountId); + final receiverAccount = await (await stellarSdk).accounts.account( + accountId, + ); if (receiverAccount.accountId != "") { exists = true; } } catch (e, s) { Logging.instance.e( - "Error getting account ${e.toString()} - ${s.toString()}", - error: e, - stackTrace: s); + "Error getting account ${e.toString()} - ${s.toString()}", + error: e, + stackTrace: s, + ); } return exists; } @@ -225,15 +219,17 @@ class StellarWallet extends Bip39Wallet { try { final address = await getCurrentReceivingAddress(); if (address == null) { - await mainDB - .updateOrPutAddresses([await _fetchStellarAddress(index: 0)]); + await mainDB.updateOrPutAddresses([ + await _fetchStellarAddress(index: 0), + ]); } } catch (e, s) { // do nothing, still allow user into wallet Logging.instance.e( - "$runtimeType checkSaveInitialReceivingAddress() failed: ", - error: e, - stackTrace: s); + "$runtimeType checkSaveInitialReceivingAddress() failed: ", + error: e, + stackTrace: s, + ); } } @@ -245,7 +241,7 @@ class StellarWallet extends Bip39Wallet { } final feeRate = txData.feeRateType; - var fee = 1000; + BigInt fee = BigInt.from(1000); if (feeRate is FeeRateType) { final theFees = await fees; switch (feeRate) { @@ -261,13 +257,16 @@ class StellarWallet extends Bip39Wallet { return txData.copyWith( fee: Amount( - rawValue: BigInt.from(fee), + rawValue: fee, fractionDigits: cryptoCurrency.fractionDigits, ), ); } catch (e, s) { - Logging.instance - .e("$runtimeType prepareSend() failed: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType prepareSend() failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -275,8 +274,9 @@ class StellarWallet extends Bip39Wallet { @override Future confirmSend({required TxData txData}) async { final senderKeyPair = await _getSenderKeyPair(index: 0); - final sender = - await (await stellarSdk).accounts.account(senderKeyPair.accountId); + final sender = await (await stellarSdk).accounts.account( + senderKeyPair.accountId, + ); final address = txData.recipients!.first.address; final amountToSend = txData.recipients!.first.amount; @@ -293,9 +293,9 @@ class StellarWallet extends Bip39Wallet { address, amountToSend.decimal.toString(), ); - transactionBuilder = stellar.TransactionBuilder(sender).addOperation( - createAccBuilder.build(), - ); + transactionBuilder = stellar.TransactionBuilder( + sender, + ).addOperation(createAccBuilder.build()); } else { transactionBuilder = stellar.TransactionBuilder(sender).addOperation( stellar.PaymentOperationBuilder( @@ -316,14 +316,13 @@ class StellarWallet extends Bip39Wallet { try { final response = await (await stellarSdk).submitTransaction(transaction); if (!response.success) { - throw Exception("${response.extras?.resultCodes?.transactionResultCode}" - " ::: ${response.extras?.resultCodes?.operationsResultCodes}"); + throw Exception( + "${response.extras?.resultCodes?.transactionResultCode}" + " ::: ${response.extras?.resultCodes?.operationsResultCodes}", + ); } - return txData.copyWith( - txHash: response.hash!, - txid: response.hash!, - ); + return txData.copyWith(txHash: response.hash!, txid: response.hash!); } catch (e, s) { Logging.instance.e("Error sending TX $e - $s", error: e, stackTrace: s); rethrow; @@ -331,17 +330,17 @@ class StellarWallet extends Bip39Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final baseFee = await _getBaseFee(); return Amount( - rawValue: BigInt.from(baseFee), + rawValue: baseFee, fractionDigits: cryptoCurrency.fractionDigits, ); } @override Future get fees async { - final int fee = await _getBaseFee(); + final fee = await _getBaseFee(); return FeeObject( numberOfBlocksFast: 1, numberOfBlocksAverage: 1, @@ -379,16 +378,17 @@ class StellarWallet extends Bip39Wallet { stellar.AccountResponse accountResponse; try { - accountResponse = await (await stellarSdk) - .accounts + accountResponse = await (await stellarSdk).accounts .account((await getCurrentReceivingAddress())!.value) .onError((error, stackTrace) => throw error!); } catch (e) { if (e is stellar.ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { + e.body.contains( + "The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.", + )) { // probably just doesn't have any history yet or whatever stellar needs return; } else { @@ -438,16 +438,18 @@ class StellarWallet extends Bip39Wallet { @override Future updateChainHeight() async { try { - final height = await (await stellarSdk) - .ledgers + final height = await (await stellarSdk).ledgers .order(stellar.RequestBuilderOrder.DESC) .limit(1) .execute() .then((value) => value.records!.first.sequence); await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { - Logging.instance.e("$runtimeType updateChainHeight() failed: ", - error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType updateChainHeight() failed: ", + error: e, + stackTrace: s, + ); rethrow; } @@ -467,17 +469,19 @@ class StellarWallet extends Bip39Wallet { final List transactionList = []; stellar.Page payments; try { - payments = await (await stellarSdk) - .payments - .forAccount(myAddress.value) - .order(stellar.RequestBuilderOrder.DESC) - .execute(); + payments = + await (await stellarSdk).payments + .forAccount(myAddress.value) + .order(stellar.RequestBuilderOrder.DESC) + .execute(); } catch (e) { if (e is stellar.ErrorResponse && - e.body.contains("The resource at the url requested was not found. " - "This usually occurs for one of two reasons: " - "The url requested is not valid, or no data in our database " - "could be found with the parameters provided.")) { + e.body.contains( + "The resource at the url requested was not found. " + "This usually occurs for one of two reasons: " + "The url requested is not valid, or no data in our database " + "could be found with the parameters provided.", + )) { // probably just doesn't have any history yet or whatever stellar needs return; } else { @@ -521,13 +525,11 @@ class StellarWallet extends Bip39Wallet { final OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor( - scriptPubKeyHex: "00", - valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], - walletOwns: addressTo == myAddress.value, - ); + scriptPubKeyHex: "00", + valueStringSats: amount.raw.toString(), + addresses: [addressTo], + walletOwns: addressTo == myAddress.value, + ); final InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor( scriptSigHex: null, scriptSigAsm: null, @@ -558,10 +560,11 @@ class StellarWallet extends Bip39Wallet { } final otherData = { - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( @@ -603,8 +606,8 @@ class StellarWallet extends Bip39Wallet { final List outputs = []; final List inputs = []; - final OutputV2 output = - OutputV2.isarCantDoRequiredInDefaultConstructor( + final OutputV2 + output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), addresses: [ @@ -634,19 +637,20 @@ class StellarWallet extends Bip39Wallet { int fee = 0; int height = 0; - final tx = await (await stellarSdk) - .transactions - .transaction(caor.transactionHash!); + final tx = await (await stellarSdk).transactions.transaction( + caor.transactionHash!, + ); if (tx.hash.isNotEmpty) { fee = tx.feeCharged!; height = tx.ledger; } final otherData = { - "overrideFee": Amount( - rawValue: BigInt.from(fee), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + "overrideFee": + Amount( + rawValue: BigInt.from(fee), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), }; final theTransaction = TransactionV2( @@ -671,8 +675,11 @@ class StellarWallet extends Bip39Wallet { await mainDB.updateOrPutTransactionV2s(transactionList); } catch (e, s) { - Logging.instance.e("Exception rethrown from updateTransactions(): ", - error: e, stackTrace: s); + Logging.instance.e( + "Exception rethrown from updateTransactions(): ", + error: e, + stackTrace: s, + ); rethrow; } } diff --git a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart index 845b0282b..d32ae3db8 100644 --- a/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart +++ b/lib/wallets/wallet/impl/sub_wallets/eth_token_wallet.dart @@ -5,7 +5,6 @@ import 'package:isar/isar.dart'; import 'package:web3dart/web3dart.dart' as web3dart; import '../../../../dto/ethereum/eth_token_tx_dto.dart'; -import '../../../../dto/ethereum/eth_token_tx_extra_dto.dart'; import '../../../../models/balance.dart'; import '../../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -15,7 +14,6 @@ import '../../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../../models/paymint/fee_object_model.dart'; import '../../../../services/ethereum/ethereum_api.dart'; import '../../../../utilities/amount/amount.dart'; -import '../../../../utilities/enums/fee_rate_type_enum.dart'; import '../../../../utilities/eth_commons.dart'; import '../../../../utilities/extensions/extensions.dart'; import '../../../../utilities/logger.dart'; @@ -29,7 +27,7 @@ class EthTokenWallet extends Wallet { int get isarTransactionVersion => 2; EthTokenWallet(this.ethWallet, this._tokenContract) - : super(ethWallet.cryptoCurrency); + : super(ethWallet.cryptoCurrency); final EthereumWallet ethWallet; @@ -39,8 +37,6 @@ class EthTokenWallet extends Wallet { late web3dart.DeployedContract _deployedContract; late web3dart.ContractFunction _sendFunction; - static const _gasLimit = 65000; - // =========================================================================== // =========================================================================== @@ -85,9 +81,7 @@ class EthTokenWallet extends Wallet { final output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == myAddress, ); final input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -116,16 +110,15 @@ class EthTokenWallet extends Wallet { inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, - type: addressTo == myAddress - ? TransactionType.sentToSelf - : TransactionType.outgoing, + type: + addressTo == myAddress + ? TransactionType.sentToSelf + : TransactionType.outgoing, subType: TransactionSubType.ethToken, otherData: jsonEncode(otherData), ); - return txData.copyWith( - tempTx: tempTx, - ); + return txData.copyWith(tempTx: tempTx); } // =========================================================================== @@ -143,8 +136,9 @@ class EthTokenWallet extends Wallet { try { await super.init(); - final contractAddress = - web3dart.EthereumAddress.fromHex(tokenContract.address); + final contractAddress = web3dart.EthereumAddress.fromHex( + tokenContract.address, + ); // first try to update the abi regardless just in case something has changed try { @@ -153,8 +147,11 @@ class EthTokenWallet extends Wallet { usingContractAddress: contractAddress.hex, ); } catch (e, s) { - Logging.instance - .w("$runtimeType _updateTokenABI(): ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType _updateTokenABI(): ", + error: e, + stackTrace: s, + ); } try { @@ -176,8 +173,8 @@ class EthTokenWallet extends Wallet { // Some failure, try for proxy contract final contractAddressResponse = await EthereumAPI.getProxyTokenImplementationAddress( - contractAddress.hex, - ); + contractAddress.hex, + ); if (contractAddressResponse.value != null) { _tokenContract = await _updateTokenABI( @@ -198,55 +195,36 @@ class EthTokenWallet extends Wallet { _sendFunction = _deployedContract.function('transfer'); } catch (e, s) { - Logging.instance - .w("$runtimeType wallet failed init(): ", error: e, stackTrace: s); + Logging.instance.w( + "$runtimeType wallet failed init(): ", + error: e, + stackTrace: s, + ); } } @override Future prepareSend({required TxData txData}) async { - final feeRateType = txData.feeRateType!; - int fee = 0; - final feeObject = await fees; - switch (feeRateType) { - case FeeRateType.fast: - fee = feeObject.fast; - break; - case FeeRateType.average: - fee = feeObject.medium; - break; - case FeeRateType.slow: - fee = feeObject.slow; - break; - case FeeRateType.custom: - throw UnimplementedError("custom eth token fees"); - } - - final feeEstimate = await estimateFeeFor(Amount.zero, fee); - - final client = ethWallet.getEthClient(); - - final myAddress = (await getCurrentReceivingAddress())!.value; - final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); - - final nonce = txData.nonce ?? - await client.getTransactionCount( - myWeb3Address, - atBlock: const web3dart.BlockNum.pending(), - ); - final amount = txData.recipients!.first.amount; final address = txData.recipients!.first.address; - await updateBalance(); - final info = await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); - final availableBalance = info?.getCachedBalance().spendable ?? - Amount.zeroWith( - fractionDigits: tokenContract.decimals, - ); + final myWeb3Address = await ethWallet.getMyWeb3Address(); + + final prep = await ethWallet.internalSharedPrepareSend( + txData: txData, + myWeb3Address: myWeb3Address, + ); + + // double check balance after internalSharedPrepareSend call to ensure + // balance is up to date + final info = + await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); + final availableBalance = + info?.getCachedBalance().spendable ?? + Amount.zeroWith(fractionDigits: tokenContract.decimals); if (amount > availableBalance) { throw Exception("Insufficient balance"); } @@ -255,19 +233,26 @@ class EthTokenWallet extends Wallet { contract: _deployedContract, function: _sendFunction, parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], - maxGas: _gasLimit, - gasPrice: web3dart.EtherAmount.fromUnitAndValue( + maxGas: txData.ethEIP1559Fee?.gasLimit ?? kEthereumTokenMinGasLimit, + nonce: prep.nonce, + maxFeePerGas: web3dart.EtherAmount.fromBigInt( web3dart.EtherUnit.wei, - fee, + prep.maxBaseFee, + ), + maxPriorityFeePerGas: web3dart.EtherAmount.fromBigInt( + web3dart.EtherUnit.wei, + prep.priorityFee, ), - nonce: nonce, ); + final feeEstimate = await estimateFeeFor( + Amount.zero, + prep.maxBaseFee + prep.priorityFee, + ); return txData.copyWith( fee: feeEstimate, - feeInWei: BigInt.from(fee), web3dartTransaction: tx, - chainId: await client.getChainId(), + chainId: prep.chainId, nonce: tx.nonce, ); } @@ -286,16 +271,16 @@ class EthTokenWallet extends Wallet { } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { return ethWallet.estimateEthFee( feeRate, - _gasLimit, + kEthereumTokenMinGasLimit, cryptoCurrency.fractionDigits, ); } @override - Future get fees => EthereumAPI.getFees(); + Future get fees => EthereumAPI.getFees(); @override Future pingCheck() async { @@ -317,10 +302,11 @@ class EthTokenWallet extends Wallet { @override Future updateBalance() async { try { - final info = await mainDB.isar.tokenWalletInfo - .where() - .walletIdTokenAddressEqualTo(walletId, tokenContract.address) - .findFirst(); + final info = + await mainDB.isar.tokenWalletInfo + .where() + .walletIdTokenAddressEqualTo(walletId, tokenContract.address) + .findFirst(); final response = await EthereumAPI.getWalletTokenBalance( address: (await getCurrentReceivingAddress())!.value, contractAddress: tokenContract.address, @@ -364,8 +350,9 @@ class EthTokenWallet extends Wallet { @override Future updateTransactions() async { try { - final String addressString = - checksumEthereumAddress((await getCurrentReceivingAddress())!.value); + final String addressString = checksumEthereumAddress( + (await getCurrentReceivingAddress())!.value, + ); final response = await EthereumAPI.getTokenTransactions( address: addressString, @@ -374,8 +361,9 @@ class EthTokenWallet extends Wallet { if (response.value == null) { if (response.exception != null && - response.exception!.message - .contains("response is empty but status code is 200")) { + response.exception!.message.contains( + "response is empty but status code is 200", + )) { Logging.instance.d( "No ${tokenContract.name} transfers found for $addressString", ); @@ -390,50 +378,38 @@ class EthTokenWallet extends Wallet { return; } - final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( - response.value!.map((e) => e.transactionHash).toSet().toList(), - ); - - if (response2.value == null) { - throw response2.exception ?? - Exception("Failed to fetch token transactions"); - } - final List<({EthTokenTxDto tx, EthTokenTxExtraDTO extra})> data = []; - for (final tokenDto in response.value!) { - try { - final txExtra = response2.value!.firstWhere( - (e) => e.hash == tokenDto.transactionHash, - ); - data.add( - ( - tx: tokenDto, - extra: txExtra, - ), - ); - } catch (e, s) { - // Server indexing failed for some reason. Instead of hard crashing or - // showing no transactions we just skip it here. Not ideal but better - // than nothing showing up - Logging.instance.e( - "Server error: Transaction hash not found.", - error: e, - stackTrace: s, - ); - Logging.instance.d( - "Server error: Transaction ${tokenDto.transactionHash} not found.", - error: e, - stackTrace: s, - ); + web3dart.Web3Client? client; + final List allTxs = []; + for (final dto in response.value!) { + if (dto.nonce == null) { + client ??= ethWallet.getEthClient(); + final txInfo = await client.getTransactionByHash(dto.transactionHash); + if (txInfo == null) { + // Something strange is happening + Logging.instance.w( + "Could not find token transaction via RPC that was found use " + "TrueBlocks API.\nOffending tx: $dto", + ); + } else { + final updated = dto.copyWith( + nonce: txInfo.nonce, + gasPrice: txInfo.gasPrice.getInWei, + gasUsed: txInfo.gas, + ); + allTxs.add(updated); + } + } else { + allTxs.add(dto); } } final List txns = []; - for (final tuple in data) { + for (final tx in allTxs) { // ignore all non Transfer events (for now) - if (tuple.tx.topics[0] == kTransferEventSignature) { + if (tx.topics[0] == kTransferEventSignature) { final amount = Amount( - rawValue: tuple.tx.data.toBigIntFromHex, + rawValue: tx.data.toBigIntFromHex, fractionDigits: tokenContract.decimals, ); @@ -442,13 +418,12 @@ class EthTokenWallet extends Wallet { continue; } - final Amount txFee = tuple.extra.gasUsed * tuple.extra.gasPrice; - final addressFrom = _addressFromTopic( - tuple.tx.topics[1], - ); - final addressTo = _addressFromTopic( - tuple.tx.topics[2], + final txFee = Amount( + rawValue: BigInt.from(tx.gasUsed!) * tx.gasPrice!, + fractionDigits: cryptoCurrency.fractionDigits, ); + final addressFrom = _addressFromTopic(tx.topics[1]); + final addressTo = _addressFromTopic(tx.topics[2]); final TransactionType txType; if (addressTo == addressString) { @@ -470,10 +445,10 @@ class EthTokenWallet extends Wallet { } final otherData = { - "nonce": tuple.extra.nonce, + "nonce": tx.nonce, "isCancelled": false, "overrideFee": txFee.toJsonString(), - "contractAddress": tuple.tx.address, + "contractAddress": tx.address, }; // hack eth tx data into inputs and outputs @@ -483,9 +458,7 @@ class EthTokenWallet extends Wallet { final output = OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "00", valueStringSats: amount.raw.toString(), - addresses: [ - addressTo, - ], + addresses: [addressTo], walletOwns: addressTo == addressString, ); final input = InputV2.isarCantDoRequiredInDefaultConstructor( @@ -506,11 +479,11 @@ class EthTokenWallet extends Wallet { final txn = TransactionV2( walletId: walletId, - blockHash: tuple.extra.blockHash, - hash: tuple.tx.transactionHash, - txid: tuple.tx.transactionHash, - timestamp: tuple.extra.timestamp, - height: tuple.tx.blockNumber, + blockHash: tx.blockHash, + hash: tx.transactionHash, + txid: tx.transactionHash, + timestamp: tx.timestamp, + height: tx.blockNumber, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), version: -1, @@ -544,15 +517,15 @@ class EthTokenWallet extends Wallet { @override FilterOperation? get transactionFilterOperation => FilterGroup.and([ - FilterCondition.equalTo( - property: r"contractAddress", - value: tokenContract.address, - ), - const FilterCondition.equalTo( - property: r"subType", - value: TransactionSubType.ethToken, - ), - ]); + FilterCondition.equalTo( + property: r"contractAddress", + value: tokenContract.address, + ), + const FilterCondition.equalTo( + property: r"subType", + value: TransactionSubType.ethToken, + ), + ]); @override Future checkSaveInitialReceivingAddress() async { diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 992a900ae..ae2183be0 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -115,9 +115,10 @@ class TezosWallet extends Bip39Wallet { prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null; final tezartClient = tezart.TezartClient( server, - proxy: proxyInfo != null - ? "socks5://${proxyInfo.host}:${proxyInfo.port};" - : null, + proxy: + proxyInfo != null + ? "socks5://${proxyInfo.host}:${proxyInfo.port};" + : null, ); final opList = await tezartClient.transferOperation( @@ -197,9 +198,7 @@ class TezosWallet extends Bip39Wallet { } final myAddress = (await getCurrentReceivingAddress())!; - final account = await TezosAPI.getAccount( - myAddress.value, - ); + final account = await TezosAPI.getAccount(myAddress.value); // final bool isSendAll = sendAmount == info.cachedBalance.spendable; // @@ -262,13 +261,8 @@ class TezosWallet extends Bip39Wallet { // fee: fee, fee: Amount( rawValue: opList.operations - .map( - (e) => BigInt.from(e.fee), - ) - .fold( - BigInt.zero, - (p, e) => p + e, - ), + .map((e) => BigInt.from(e.fee)) + .fold(BigInt.zero, (p, e) => p + e), fractionDigits: cryptoCurrency.fractionDigits, ), tezosOperationsList: opList, @@ -280,14 +274,14 @@ class TezosWallet extends Bip39Wallet { stackTrace: s, ); - if (e - .toString() - .contains("(_operationResult['errors']): Must not be null")) { + if (e.toString().contains( + "(_operationResult['errors']): Must not be null", + )) { throw Exception("Probably insufficient balance"); } else if (e.toString().contains( - "The simulation of the operation: \"transaction\" failed with error(s) :" - " contract.balance_too_low, tez.subtraction_underflow.", - )) { + "The simulation of the operation: \"transaction\" failed with error(s) :" + " contract.balance_too_low, tez.subtraction_underflow.", + )) { throw Exception("Insufficient balance to pay fees"); } @@ -300,9 +294,7 @@ class TezosWallet extends Bip39Wallet { _hackedCheckTorNodePrefs(); await txData.tezosOperationsList!.inject(); await txData.tezosOperationsList!.monitor(); - return txData.copyWith( - txid: txData.tezosOperationsList!.result.id, - ); + return txData.copyWith(txid: txData.tezosOperationsList!.result.id); } int _estCount = 0; @@ -350,13 +342,8 @@ class TezosWallet extends Bip39Wallet { rethrow; } else { _estCount++; - Logging.instance.e( - "_estimate() retry _estCount=$_estCount", - ); - return await _estimate( - account, - recipientAddress, - ); + Logging.instance.e("_estimate() retry _estCount=$_estCount"); + return await _estimate(account, recipientAddress); } } } @@ -364,7 +351,7 @@ class TezosWallet extends Bip39Wallet { @override Future estimateFeeFor( Amount amount, - int feeRate, { + BigInt feeRate, { String recipientAddress = "tz1MXvDCyXSqBqXPNDcsdmVZKfoxL9FTHmp2", }) async { _hackedCheckTorNodePrefs(); @@ -376,9 +363,7 @@ class TezosWallet extends Bip39Wallet { } final myAddress = (await getCurrentReceivingAddress())!; - final account = await TezosAPI.getAccount( - myAddress.value, - ); + final account = await TezosAPI.getAccount(myAddress.value); try { final fees = await _estimate(account, recipientAddress); @@ -402,7 +387,7 @@ class TezosWallet extends Bip39Wallet { /// Not really used (yet) @override Future get fees async { - const feePerTx = 1; + final feePerTx = BigInt.one; return FeeObject( numberOfBlocksFast: 10, numberOfBlocksAverage: 10, @@ -418,10 +403,7 @@ class TezosWallet extends Bip39Wallet { _hackedCheckTorNodePrefs(); final currentNode = getCurrentNode(); return await TezosRpcAPI.testNetworkConnection( - nodeInfo: ( - host: currentNode.host, - port: currentNode.port, - ), + nodeInfo: (host: currentNode.host, port: currentNode.port), ); } @@ -518,16 +500,10 @@ class TezosWallet extends Bip39Wallet { _hackedCheckTorNodePrefs(); final currentNode = _xtzNode ?? getCurrentNode(); final height = await TezosRpcAPI.getChainHeight( - nodeInfo: ( - host: currentNode.host, - port: currentNode.port, - ), + nodeInfo: (host: currentNode.host, port: currentNode.port), ); - await info.updateCachedChainHeight( - newHeight: height!, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height!, isar: mainDB.isar); } catch (e, s) { Logging.instance.e( "Error occurred in tezos_wallet.dart while getting" @@ -540,8 +516,10 @@ class TezosWallet extends Bip39Wallet { @override Future updateNode() async { - _xtzNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? + _xtzNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? info.coin.defaultNode; await refresh(); @@ -550,9 +528,10 @@ class TezosWallet extends Bip39Wallet { @override NodeModel getCurrentNode() { return _xtzNode ??= - NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? - info.coin.defaultNode; + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? + info.coin.defaultNode; } @override @@ -590,10 +569,11 @@ class TezosWallet extends Bip39Wallet { type: txType, subType: TransactionSubType.none, amount: theTx.amountInMicroTez, - amountString: Amount( - rawValue: BigInt.from(theTx.amountInMicroTez), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(theTx.amountInMicroTez), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), fee: theTx.feeInMicroTez, height: theTx.height, isCancelled: false, diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index 73b1c4f7b..e1c09b693 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -14,7 +14,7 @@ class WowneroWallet extends LibMoneroWallet { : super(Wownero(network), lib_monero_compat.WalletType.wownero); @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { if (libMoneroWallet == null || syncStatus is! lib_monero_compat.SyncedSyncStatus) { return Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits); @@ -22,7 +22,7 @@ class WowneroWallet extends LibMoneroWallet { lib_monero.TransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; - switch (feeRate) { + switch (feeRate.toInt()) { case 1: priority = lib_monero.TransactionPriority.low; feeRateType = FeeRateType.average; diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart index 352b076f7..2605d40ca 100644 --- a/lib/wallets/wallet/impl/xelis_wallet.dart +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -622,9 +622,9 @@ class XelisWallet extends LibXelisWallet { numberOfBlocksFast: 10, numberOfBlocksAverage: 10, numberOfBlocksSlow: 10, - fast: 1, - medium: 1, - slow: 1, + fast: BigInt.one, + medium: BigInt.one, + slow: BigInt.one, ); } @@ -661,7 +661,7 @@ class XelisWallet extends LibXelisWallet { // Estimate fee using the shared method final boostedFee = await estimateFeeFor( totalSendAmount, - 1, + BigInt.one, feeMultiplier: 1.0, recipients: recipients, assetId: asset, @@ -701,7 +701,7 @@ class XelisWallet extends LibXelisWallet { @override Future estimateFeeFor( Amount amount, - int feeRate, { + BigInt feeRate, { double? feeMultiplier, List recipients = const [], String? assetId, diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 55812fe19..b81a10d97 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -520,7 +520,11 @@ abstract class LibMoneroWallet trusted: node.trusted ?? false, useSSL: node.useSSL, socksProxyAddress: - proxy == null ? null : "${proxy.host.address}:${proxy.port}", + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", ); }); } else { @@ -531,7 +535,11 @@ abstract class LibMoneroWallet trusted: node.trusted ?? false, useSSL: node.useSSL, socksProxyAddress: - proxy == null ? null : "${proxy.host.address}:${proxy.port}", + node.forceNoTor + ? null + : proxy == null + ? null + : "${proxy.host.address}:${proxy.port}", ); } libMoneroWallet?.startSyncing(); @@ -1230,9 +1238,9 @@ abstract class LibMoneroWallet numberOfBlocksFast: 10, numberOfBlocksAverage: 15, numberOfBlocksSlow: 20, - fast: lib_monero.TransactionPriority.high.value, - medium: lib_monero.TransactionPriority.medium.value, - slow: lib_monero.TransactionPriority.normal.value, + fast: BigInt.from(lib_monero.TransactionPriority.high.value), + medium: BigInt.from(lib_monero.TransactionPriority.medium.value), + slow: BigInt.from(lib_monero.TransactionPriority.normal.value), ); @override diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index be21c5282..9ed0c9c6e 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -127,11 +127,7 @@ abstract class Wallet { await updateChainHeight(); } catch (e, s) { // do nothing on failure (besides logging) - Logging.instance.w( - "$e\n$s", - error: e, - stackTrace: s, - ); + Logging.instance.w("$e\n$s", error: e, stackTrace: s); } // return regardless of whether it was updated or not as we want a @@ -173,7 +169,8 @@ abstract class Wallet { value: viewOnlyData!.toJsonEncodedString(), ); } else if (wallet is MnemonicInterface) { - if (wallet is CryptonoteWallet || wallet is XelisWallet) { // + if (wallet is CryptonoteWallet || wallet is XelisWallet) { + // // currently a special case due to the xmr/wow/xelis libraries handling their // own mnemonic generation on new wallet creation // if its a restore we must set them @@ -238,10 +235,11 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final walletInfo = await mainDB.isar.walletInfo - .where() - .walletIdEqualTo(walletId) - .findFirst(); + final walletInfo = + await mainDB.isar.walletInfo + .where() + .walletIdEqualTo(walletId) + .findFirst(); Logging.instance.i( "Wallet.load loading" @@ -270,10 +268,7 @@ abstract class Wallet { required EthereumWallet ethWallet, required EthContract contract, }) { - final Wallet wallet = EthTokenWallet( - ethWallet, - contract, - ); + final Wallet wallet = EthTokenWallet(ethWallet, contract); wallet.prefs = ethWallet.prefs; wallet.nodeService = ethWallet.nodeService; @@ -287,27 +282,19 @@ abstract class Wallet { // ========== Static Util ==================================================== // secure storage key - static String mnemonicKey({ - required String walletId, - }) => + static String mnemonicKey({required String walletId}) => "${walletId}_mnemonic"; // secure storage key - static String mnemonicPassphraseKey({ - required String walletId, - }) => + static String mnemonicPassphraseKey({required String walletId}) => "${walletId}_mnemonicPassphrase"; // secure storage key - static String privateKeyKey({ - required String walletId, - }) => + static String privateKeyKey({required String walletId}) => "${walletId}_privateKey"; // secure storage key - static String getViewOnlyWalletDataSecStoreKey({ - required String walletId, - }) => + static String getViewOnlyWalletDataSecStoreKey({required String walletId}) => "${walletId}_viewOnlyWalletData"; //============================================================================ @@ -321,9 +308,7 @@ abstract class Wallet { required NodeService nodeService, required Prefs prefs, }) async { - final Wallet wallet = _loadWallet( - walletInfo: walletInfo, - ); + final Wallet wallet = _loadWallet(walletInfo: walletInfo); wallet.prefs = prefs; wallet.nodeService = nodeService; @@ -339,9 +324,7 @@ abstract class Wallet { .._walletId = walletInfo.walletId; } - static Wallet _loadWallet({ - required WalletInfo walletInfo, - }) { + static Wallet _loadWallet({required WalletInfo walletInfo}) { final net = walletInfo.coin.network; switch (walletInfo.coin.runtimeType) { case const (Banano): @@ -421,12 +404,11 @@ abstract class Wallet { _periodicPingCheck(); // then periodically check - _networkAliveTimer = Timer.periodic( - Constants.networkAliveTimerDuration, - (_) async { - _periodicPingCheck(); - }, - ); + _networkAliveTimer = Timer.periodic(Constants.networkAliveTimerDuration, ( + _, + ) async { + _periodicPingCheck(); + }); } void _periodicPingCheck() async { @@ -438,15 +420,12 @@ abstract class Wallet { final bool hasNetwork = await pingCheck(); if (_isConnected != hasNetwork) { - final NodeConnectionStatus status = hasNetwork - ? NodeConnectionStatus.connected - : NodeConnectionStatus.disconnected; + final NodeConnectionStatus status = + hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - status, - walletId, - cryptoCurrency, - ), + NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), ); _isConnected = hasNetwork; @@ -499,7 +478,7 @@ abstract class Wallet { /// updates the wallet info's cachedChainHeight Future updateChainHeight(); - Future estimateFeeFor(Amount amount, int feeRate); + Future estimateFeeFor(Amount amount, BigInt feeRate); Future get fees; @@ -518,7 +497,8 @@ abstract class Wallet { } NodeModel getCurrentNode() { - final node = nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? + final node = + nodeService.getPrimaryNodeFor(currency: cryptoCurrency) ?? cryptoCurrency.defaultNode; return node; @@ -538,8 +518,9 @@ abstract class Wallet { ); if (shouldAutoSync) { - _periodicRefreshTimer ??= - Timer.periodic(const Duration(seconds: 150), (timer) async { + _periodicRefreshTimer ??= Timer.periodic(const Duration(seconds: 150), ( + timer, + ) async { // chain height check currently broken // if ((await chainHeight) != (await storedChainHeight)) { @@ -596,7 +577,8 @@ abstract class Wallet { } final start = DateTime.now(); - final viewOnly = this is ViewOnlyOptionInterface && + final viewOnly = + this is ViewOnlyOptionInterface && (this as ViewOnlyOptionInterface).isViewOnly; try { @@ -621,8 +603,9 @@ abstract class Wallet { final Set codesToCheck = {}; if (this is PaynymInterface && !viewOnly) { // isSegwit does not matter here at all - final myCode = - await (this as PaynymInterface).getPaymentCode(isSegwit: false); + final myCode = await (this as PaynymInterface).getPaymentCode( + isSegwit: false, + ); final nym = await PaynymIsApi().nym(myCode.toString()); if (nym.value != null) { @@ -685,8 +668,9 @@ abstract class Wallet { // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) { - await (this as PaynymInterface) - .checkForNotificationTransactionsTo(codesToCheck); + await (this as PaynymInterface).checkForNotificationTransactionsTo( + codesToCheck, + ); // check utxos again for notification outputs await updateUTXOs(); } @@ -746,10 +730,11 @@ abstract class Wallet { // Check if there's another wallet of this coin on the sync list. final List walletIds = []; for (final id in prefs.walletIdsSyncOnStartup) { - final wallet = mainDB.isar.walletInfo - .where() - .walletIdEqualTo(id) - .findFirstSync()!; + final wallet = + mainDB.isar.walletInfo + .where() + .walletIdEqualTo(id) + .findFirstSync()!; if (wallet.coin == cryptoCurrency) { walletIds.add(id); @@ -802,17 +787,11 @@ abstract class Wallet { return await mainDB.isar.addresses .buildQuery
( whereClauses: [ - IndexWhereClause.equalTo( - indexName: r"walletId", - value: [walletId], - ), + IndexWhereClause.equalTo(indexName: r"walletId", value: [walletId]), ], filter: filterOperation, sortBy: [ - const SortProperty( - property: r"derivationIndex", - sort: Sort.desc, - ), + const SortProperty(property: r"derivationIndex", sort: Sort.desc), ], ) .findFirst(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index e393ceb9d..8fe157a61 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -439,7 +439,7 @@ mixin ElectrumXInterface required BigInt satoshisBeingUsed, required List utxoSigningData, required int? satsPerVByte, - required int feeRatePerKB, + required BigInt feeRatePerKB, }) async { Logging.instance.d("Attempting to send all $cryptoCurrency"); if (txData.recipients!.length != 1) { @@ -1225,17 +1225,17 @@ mixin ElectrumXInterface Amount.fromDecimal( fast, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, medium: Amount.fromDecimal( medium, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, slow: Amount.fromDecimal( slow, fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + ).raw, ); Logging.instance.d("fetched fees: $feeObject"); @@ -1256,7 +1256,7 @@ mixin ElectrumXInterface } @override - Future estimateFeeFor(Amount amount, int feeRate) async { + Future estimateFeeFor(Amount amount, BigInt feeRate) async { final available = info.cachedBalance.spendable; final utxos = _spendableUTXOs(await mainDB.getUTXOs(walletId).findAll()); @@ -1713,7 +1713,7 @@ mixin ElectrumXInterface } final result = await coinSelection( - txData: txData.copyWith(feeRateAmount: -1), + txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, @@ -1729,10 +1729,10 @@ mixin ElectrumXInterface } return result; - } else if (feeRateType is FeeRateType || feeRateAmount is int) { - late final int rate; + } else if (feeRateType is FeeRateType || feeRateAmount is BigInt) { + late final BigInt rate; if (feeRateType is FeeRateType) { - int fee = 0; + BigInt fee = BigInt.zero; final feeObject = await fees; switch (feeRateType) { case FeeRateType.fast: @@ -1749,7 +1749,7 @@ mixin ElectrumXInterface } rate = fee; } else { - rate = feeRateAmount as int; + rate = feeRateAmount!; } // check for send all @@ -1832,8 +1832,8 @@ mixin ElectrumXInterface // =========================================================================== // ========== Interface functions ============================================ - int estimateTxFee({required int vSize, required int feeRatePerKB}); - Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB); + int estimateTxFee({required int vSize, required BigInt feeRatePerKB}); + Amount roughFeeEstimate(int inputCount, int outputCount, BigInt feeRatePerKB); Future> fetchAddressesForElectrumXScan(); @@ -1864,7 +1864,10 @@ mixin ElectrumXInterface .toList(); } - Future _sweepAllEstimate(int feeRate, List usableUTXOs) async { + Future _sweepAllEstimate( + BigInt feeRate, + List usableUTXOs, + ) async { final available = usableUTXOs .map((e) => BigInt.from(e.value)) .fold(BigInt.zero, (p, e) => p + e); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart index 148bd61a2..1a7937113 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart @@ -48,46 +48,50 @@ mixin LelantusInterface } Future> _getLelantusEntry() async { - final List lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .group( - (q) => q - .valueEqualTo("0") - .or() - .anonymitySetIdEqualTo(LelantusFfiWrapper.ANONYMITY_SET_EMPTY_ID), - ) - .findAll(); + final List lelantusCoins = + await mainDB.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .isUsedEqualTo(false) + .not() + .group( + (q) => q + .valueEqualTo("0") + .or() + .anonymitySetIdEqualTo( + LelantusFfiWrapper.ANONYMITY_SET_EMPTY_ID, + ), + ) + .findAll(); final root = await getRootHDNode(); - final waitLelantusEntries = lelantusCoins.map((coin) async { - final derivePath = cryptoCurrency.constructDerivePath( - derivePathType: DerivePathType.bip44, - chain: LelantusFfiWrapper.MINT_INDEX, - index: coin.mintIndex, - ); + final waitLelantusEntries = + lelantusCoins.map((coin) async { + final derivePath = cryptoCurrency.constructDerivePath( + derivePathType: DerivePathType.bip44, + chain: LelantusFfiWrapper.MINT_INDEX, + index: coin.mintIndex, + ); - try { - final keyPair = root.derivePath(derivePath); - final String privateKey = keyPair.privateKey.data.toHex; - return lelantus.DartLelantusEntry( - coin.isUsed ? 1 : 0, - 0, - coin.anonymitySetId, - int.parse(coin.value), - coin.mintIndex, - privateKey, - ); - } catch (e, s) { - Logging.instance.e("error bad key"); - Logging.instance.t("error bad key", error: e, stackTrace: s); - return lelantus.DartLelantusEntry(1, 0, 0, 0, 0, ''); - } - }).toList(); + try { + final keyPair = root.derivePath(derivePath); + final String privateKey = keyPair.privateKey.data.toHex; + return lelantus.DartLelantusEntry( + coin.isUsed ? 1 : 0, + 0, + coin.anonymitySetId, + int.parse(coin.value), + coin.mintIndex, + privateKey, + ); + } catch (e, s) { + Logging.instance.e("error bad key"); + Logging.instance.t("error bad key", error: e, stackTrace: s); + return lelantus.DartLelantusEntry(1, 0, 0, 0, 0, ''); + } + }).toList(); final lelantusEntries = await Future.wait(waitLelantusEntries); @@ -100,13 +104,9 @@ mixin LelantusInterface return lelantusEntries; } - Future prepareSendLelantus({ - required TxData txData, - }) async { + Future prepareSendLelantus({required TxData txData}) async { if (txData.recipients!.length != 1) { - throw Exception( - "Lelantus send requires a single recipient", - ); + throw Exception("Lelantus send requires a single recipient"); } if (txData.recipients!.first.amount.raw > @@ -125,8 +125,9 @@ mixin LelantusInterface isSendAll = true; } - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); + final lastUsedIndex = await mainDB.getHighestUsedMintIndex( + walletId: walletId, + ); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; final root = await getRootHDNode(); @@ -174,18 +175,15 @@ mixin LelantusInterface } } - Future confirmSendLelantus({ - required TxData txData, - }) async { + Future confirmSendLelantus({required TxData txData}) async { final latestSetId = await electrumXClient.getLelantusLatestCoinId(); - final txid = await electrumXClient.broadcastTransaction( - rawTx: txData.raw!, - ); + final txid = await electrumXClient.broadcastTransaction(rawTx: txData.raw!); assert(txid == txData.txid!); - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); + final lastUsedIndex = await mainDB.getHighestUsedMintIndex( + walletId: walletId, + ); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; if (txData.spendCoinIndexes != null) { @@ -197,10 +195,11 @@ mixin LelantusInterface // Update all of the coins that have been spent. for (final index in spentCoinIndexes) { - final possibleCoin = await mainDB.isar.lelantusCoins - .where() - .mintIndexWalletIdEqualTo(index, walletId) - .findFirst(); + final possibleCoin = + await mainDB.isar.lelantusCoins + .where() + .mintIndexWalletIdEqualTo(index, walletId) + .findFirst(); if (possibleCoin != null) { updatedCoins.add(possibleCoin.copyWith(isUsed: true)); @@ -232,11 +231,7 @@ mixin LelantusInterface await mainDB.isar.lelantusCoins.put(jmint); }); } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); rethrow; } @@ -264,7 +259,8 @@ mixin LelantusInterface numberOfMessages: null, ); - final transactionAddress = await mainDB + final transactionAddress = + await mainDB .getAddresses(walletId) .filter() .valueEqualTo(txData.recipients!.first.address) @@ -311,18 +307,12 @@ mixin LelantusInterface await mainDB.isar.lelantusCoins.putAll(updatedCoins); }); } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); rethrow; } } - return txData.copyWith( - txid: txid, - ); + return txData.copyWith(txid: txid); } Future>> fastFetch(List allTxHashes) async { @@ -334,10 +324,10 @@ mixin LelantusInterface for (final txHash in allTxHashes) { final Future> transactionFuture = electrumXCachedClient.getTransaction( - txHash: txHash, - verbose: true, - cryptoCurrency: cryptoCurrency, - ); + txHash: txHash, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); transactionFutures.add(transactionFuture); currentFutureCount++; if (currentFutureCount > futureLimit) { @@ -373,8 +363,9 @@ mixin LelantusInterface ) async { try { final Map txs = {}; - final List> allTransactions = - await fastFetch(transactions); + final List> allTransactions = await fastFetch( + transactions, + ); for (int i = 0; i < allTransactions.length; i++) { try { @@ -397,16 +388,18 @@ mixin LelantusInterface final txn = Transaction( walletId: walletId, txid: tx["txid"] as String, - timestamp: tx["time"] as int? ?? + timestamp: + tx["time"] as int? ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: TransactionType.outgoing, subType: TransactionSubType.join, amount: amount.raw.toInt(), amountString: amount.toJsonString(), - fee: Amount.fromDecimal( - Decimal.parse(tx["fees"].toString()), - fractionDigits: cryptoCurrency.fractionDigits, - ).raw.toInt(), + fee: + Amount.fromDecimal( + Decimal.parse(tx["fees"].toString()), + fractionDigits: cryptoCurrency.fractionDigits, + ).raw.toInt(), height: tx["height"] as int?, isCancelled: false, isLelantus: true, @@ -418,7 +411,8 @@ mixin LelantusInterface numberOfMessages: null, ); - final address = await mainDB + final address = + await mainDB .getAddresses(walletId) .filter() .valueEqualTo(tx["address"] as String) @@ -488,8 +482,10 @@ mixin LelantusInterface final Map setDataMap = {}; final anonymitySets = await fetchAnonymitySets(); for (int setId = 1; setId <= latestSetId; setId++) { - final setData = anonymitySets - .firstWhere((element) => element["setId"] == setId, orElse: () => {}); + final setData = anonymitySets.firstWhere( + (element) => element["setId"] == setId, + orElse: () => {}, + ); if (setData.isNotEmpty) { setDataMap[setId] = setData; @@ -500,22 +496,22 @@ mixin LelantusInterface // TODO: verify this function does what we think it does Future refreshLelantusData() async { - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .valueEqualTo(0.toString()) - .findAll(); + final lelantusCoins = + await mainDB.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .isUsedEqualTo(false) + .not() + .valueEqualTo(0.toString()) + .findAll(); final List updatedCoins = []; final usedSerialNumbersSet = (await electrumXCachedClient.getUsedCoinSerials( - cryptoCurrency: info.coin, - )) - .toSet(); + cryptoCurrency: info.coin, + )).toSet(); final root = await getRootHDNode(); @@ -563,11 +559,7 @@ mixin LelantusInterface await mainDB.isar.lelantusCoins.putAll(updatedCoins); }); } catch (e, s) { - Logging.instance.f( - " ", - error: e, - stackTrace: s, - ); + Logging.instance.f(" ", error: e, stackTrace: s); rethrow; } } @@ -607,13 +599,14 @@ mixin LelantusInterface final currentHeight = await chainHeight; - final txns = await mainDB - .getTransactions(walletId) - .filter() - .isLelantusIsNull() - .or() - .isLelantusEqualTo(false) - .findAll(); + final txns = + await mainDB + .getTransactions(walletId) + .filter() + .isLelantusIsNull() + .or() + .isLelantusEqualTo(false) + .findAll(); // TODO: [prio=high] shouldn't these be v2? If it doesn't matter than we can get rid of this logic // Edit the receive transactions with the mint fees. @@ -663,10 +656,11 @@ mixin LelantusInterface type: inputTx.type, subType: TransactionSubType.mint, amount: inputTx.amount, - amountString: Amount( - rawValue: BigInt.from(inputTx.amount), - fractionDigits: cryptoCurrency.fractionDigits, - ).toJsonString(), + amountString: + Amount( + rawValue: BigInt.from(inputTx.amount), + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), fee: sharedFee, height: inputTx.height, isCancelled: false, @@ -706,11 +700,7 @@ mixin LelantusInterface await mainDB.isar.lelantusCoins.putAll(result.lelantusCoins); }); } catch (e, s) { - Logging.instance.e( - "", - error: e, - stackTrace: s, - ); + Logging.instance.e("", error: e, stackTrace: s); // don't just rethrow since isar likes to strip stack traces for some reason throw Exception("e=$e & s=$s"); } @@ -722,13 +712,12 @@ mixin LelantusInterface } // Create the joinsplit transactions. - final spendTxs = await getJMintTransactions( - result.spendTxIds, - ); + final spendTxs = await getJMintTransactions(result.spendTxIds); Logging.instance.d("lelantus spendTxs: $spendTxs"); for (final element in spendTxs.entries) { - final address = element.value.address.value ?? + final address = + element.value.address.value ?? data[element.value.txid]?.item1 ?? element.key; // Address( @@ -747,8 +736,9 @@ mixin LelantusInterface for (final value in data.values) { final transactionAddress = value.item1!; - final outs = - value.item2.outputs.where((_) => true).toList(growable: false); + final outs = value.item2.outputs + .where((_) => true) + .toList(growable: false); final ins = value.item2.inputs.where((_) => true).toList(growable: false); txnsData.add( @@ -778,9 +768,7 @@ mixin LelantusInterface wif: cryptoCurrency.networkParams.wifPrefix, ); - final txb = bitcoindart.TransactionBuilder( - network: convertedNetwork, - ); + final txb = bitcoindart.TransactionBuilder(network: convertedNetwork); txb.setVersion(2); final int height = await chainHeight; @@ -794,42 +782,40 @@ mixin LelantusInterface switch (signingData[i].derivePathType) { case DerivePathType.bip44: - data = bitcoindart - .P2PKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2PKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip49: - final p2wpkh = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; - data = bitcoindart - .P2SH( - data: bitcoindart.PaymentData(redeem: p2wpkh), - network: convertedNetwork, - ) - .data; + final p2wpkh = + bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; + data = + bitcoindart + .P2SH( + data: bitcoindart.PaymentData(redeem: p2wpkh), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip84: - data = bitcoindart - .P2WPKH( - data: bitcoindart.PaymentData( - pubkey: pubKey, - ), - network: convertedNetwork, - ) - .data; + data = + bitcoindart + .P2WPKH( + data: bitcoindart.PaymentData(pubkey: pubKey), + network: convertedNetwork, + ) + .data; break; case DerivePathType.bip86: @@ -851,8 +837,9 @@ mixin LelantusInterface for (final mintsElement in txData.mintsMapLelantus!) { Logging.instance.d("using $mintsElement"); - final Uint8List mintu8 = - Format.stringToUint8List(mintsElement['script'] as String); + final Uint8List mintu8 = Format.stringToUint8List( + mintsElement['script'] as String, + ); txb.addOutput(mintu8, mintsElement['value'] as int); } @@ -922,11 +909,12 @@ mixin LelantusInterface /// Returns the mint transaction hex to mint all of the available funds. Future _mintSelection() async { final currentChainHeight = await chainHeight; - final List availableOutputs = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .findAll(); + final List availableOutputs = + await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .findAll(); final List spendableOutputs = []; // Build list of spendable outputs and totaling their satoshi amount @@ -944,21 +932,23 @@ mixin LelantusInterface } } - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .not() - .valueEqualTo(0.toString()) - .findAll(); - - final data = await mainDB - .getTransactions(walletId) - .filter() - .isLelantusIsNull() - .or() - .isLelantusEqualTo(false) - .findAll(); + final lelantusCoins = + await mainDB.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .not() + .valueEqualTo(0.toString()) + .findAll(); + + final data = + await mainDB + .getTransactions(walletId) + .filter() + .isLelantusIsNull() + .or() + .isLelantusEqualTo(false) + .findAll(); for (final value in data) { if (value.inputs.isNotEmpty) { @@ -969,8 +959,9 @@ mixin LelantusInterface orElse: () => null, ) != null) { - spendableOutputs - .removeWhere((output) => output!.txid == element.txid); + spendableOutputs.removeWhere( + (output) => output!.txid == element.txid, + ); } } } @@ -1005,10 +996,11 @@ mixin LelantusInterface final feesObject = await fees; - final Decimal fastFee = Amount( - rawValue: BigInt.from(feesObject.fast), - fractionDigits: cryptoCurrency.fractionDigits, - ).decimal; + final Decimal fastFee = + Amount( + rawValue: feesObject.fast, + fractionDigits: cryptoCurrency.fractionDigits, + ).decimal; int firoFee = (dvSize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); // int firoFee = (vSize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); @@ -1022,9 +1014,7 @@ mixin LelantusInterface final mintsWithFee = await _createMintsFromAmount(satoshiAmountToSend); txData = await buildMintTransaction( - txData: txData.copyWith( - mintsMapLelantus: mintsWithFee, - ), + txData: txData.copyWith(mintsMapLelantus: mintsWithFee), ); return txData; @@ -1039,8 +1029,9 @@ mixin LelantusInterface int tmpTotal = total; int counter = 0; - final lastUsedIndex = - await mainDB.getHighestUsedMintIndex(walletId: walletId); + final lastUsedIndex = await mainDB.getHighestUsedMintIndex( + walletId: walletId, + ); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; final isTestnet = cryptoCurrency.network.isTestNet; @@ -1118,12 +1109,9 @@ mixin LelantusInterface isTestNet: isTestnet, ); - mints.add({ - "value": mintValue, - "script": mint, - "index": index, - }); - tmpTotal = tmpTotal - + mints.add({"value": mintValue, "script": mint, "index": index}); + tmpTotal = + tmpTotal - (isTestnet ? LelantusFfiWrapper.MINT_LIMIT_TESTNET : LelantusFfiWrapper.MINT_LIMIT); @@ -1156,32 +1144,29 @@ mixin LelantusInterface // call to super to update transparent balance final normalBalanceFuture = super.updateBalance(); - final lelantusCoins = await mainDB.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .filter() - .isUsedEqualTo(false) - .not() - .valueEqualTo(0.toString()) - .findAll(); + final lelantusCoins = + await mainDB.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .isUsedEqualTo(false) + .not() + .valueEqualTo(0.toString()) + .findAll(); final currentChainHeight = await chainHeight; int intLelantusBalance = 0; int unconfirmedLelantusBalance = 0; for (final lelantusCoin in lelantusCoins) { - final Transaction? txn = mainDB.isar.transactions - .where() - .txidWalletIdEqualTo( - lelantusCoin.txid, - walletId, - ) - .findFirstSync(); + final Transaction? txn = + mainDB.isar.transactions + .where() + .txidWalletIdEqualTo(lelantusCoin.txid, walletId) + .findFirstSync(); if (txn == null) { - Logging.instance.e( - "Transaction not found in DB for lelantus coin", - ); + Logging.instance.e("Transaction not found in DB for lelantus coin"); Logging.instance.d( "Transaction not found in DB for lelantus coin: $lelantusCoin", ); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart index 1d725afb4..4c3233de7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/nano_interface.dart @@ -28,9 +28,7 @@ import '../intermediate/bip39_wallet.dart'; const _kWorkServer = "https://nodes.nanswap.com/XNO"; Map _buildHeaders(String url) { - final result = { - 'Content-type': 'application/json', - }; + final result = {'Content-type': 'application/json'}; if (url case "https://nodes.nanswap.com/XNO" || "https://nodes.nanswap.com/BAN") { result["nodes-api-key"] = kNanoSwapRpcApiKey; @@ -52,28 +50,24 @@ mixin NanoInterface on Bip39Wallet { _hackedCheckTorNodePrefs(); return _httpClient .post( - url: Uri.parse(_kWorkServer), // this should be a - headers: _buildHeaders(_kWorkServer), - body: json.encode( - { - "action": "work_generate", - "hash": hash, - }, - ), - proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, - ) + url: Uri.parse(_kWorkServer), // this should be a + headers: _buildHeaders(_kWorkServer), + body: json.encode({"action": "work_generate", "hash": hash}), + proxyInfo: + prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, + ) .then((_httpClient) { - if (_httpClient.code == 200) { - final Map decoded = - json.decode(_httpClient.body) as Map; - if (decoded.containsKey("error")) { - throw Exception("Received error ${decoded["error"]}"); - } - return decoded["work"] as String?; - } else { - throw Exception("Received error ${_httpClient.code}"); - } - }); + if (_httpClient.code == 200) { + final Map decoded = + json.decode(_httpClient.body) as Map; + if (decoded.containsKey("error")) { + throw Exception("Received error ${decoded["error"]}"); + } + return decoded["work"] as String?; + } else { + throw Exception("Received error ${_httpClient.code}"); + } + }); } Future _getPrivateKeyFromMnemonic() async { @@ -87,8 +81,10 @@ mixin NanoInterface on Bip39Wallet { await _getPrivateKeyFromMnemonic(), ); - final addressString = - NanoAccounts.createAccount(cryptoCurrency.nanoAccountType, publicKey); + final addressString = NanoAccounts.createAccount( + cryptoCurrency.nanoAccountType, + publicKey, + ); return Address( walletId: walletId, @@ -146,8 +142,9 @@ mixin NanoInterface on Bip39Wallet { ); final balanceData = jsonDecode(balanceResponse.body); - final BigInt currentBalance = - BigInt.parse(balanceData["balance"].toString()); + final BigInt currentBalance = BigInt.parse( + balanceData["balance"].toString(), + ); final BigInt txAmount = BigInt.parse(amountRaw); final BigInt balanceAfterTx = currentBalance + txAmount; @@ -162,16 +159,19 @@ mixin NanoInterface on Bip39Wallet { // link = send block hash: final String link = blockHash; // this "linkAsAccount" is meaningless: - final String linkAsAccount = - NanoAccounts.createAccount(NanoAccountType.BANANO, blockHash); + final String linkAsAccount = NanoAccounts.createAccount( + NanoAccountType.BANANO, + blockHash, + ); // construct the receive block: final Map receiveBlock = { "type": "state", "account": publicAddress, - "previous": openBlock - ? "0000000000000000000000000000000000000000000000000000000000000000" - : frontier, + "previous": + openBlock + ? "0000000000000000000000000000000000000000000000000000000000000000" + : frontier, "representative": representative, "balance": balanceAfterTx.toString(), "link": link, @@ -320,8 +320,10 @@ mixin NanoInterface on Bip39Wallet { @override Future updateNode() async { - _cachedNode = NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? + _cachedNode = + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? info.coin.defaultNode; unawaited(refresh()); @@ -330,8 +332,9 @@ mixin NanoInterface on Bip39Wallet { @override NodeModel getCurrentNode() { return _cachedNode ?? - NodeService(secureStorageInterface: secureStorageInterface) - .getPrimaryNodeFor(currency: info.coin) ?? + NodeService( + secureStorageInterface: secureStorageInterface, + ).getPrimaryNodeFor(currency: info.coin) ?? info.coin.defaultNode; } @@ -365,11 +368,7 @@ mixin NanoInterface on Bip39Wallet { final response = await _httpClient.post( url: uri, headers: _buildHeaders(node.host), - body: jsonEncode( - { - "action": "version", - }, - ), + body: jsonEncode({"action": "version"}), proxyInfo: prefs.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); @@ -485,15 +484,9 @@ mixin NanoInterface on Bip39Wallet { } // return the hash of the transaction: - return txData.copyWith( - txid: decoded["hash"].toString(), - ); + return txData.copyWith(txid: decoded["hash"].toString()); } catch (e, s) { - Logging.instance.e( - "Error sending transaction", - error: e, - stackTrace: s, - ); + Logging.instance.e("Error sending transaction", error: e, stackTrace: s); rethrow; } } @@ -544,8 +537,9 @@ mixin NanoInterface on Bip39Wallet { ); // this should really have proper type checking and error propagation but I'm out of time - final newData = - Map.from((await jsonDecode(response.body)) as Map); + final newData = Map.from( + (await jsonDecode(response.body)) as Map, + ); if (newData["previous"] is String) { if (data?["history"] is List) { @@ -572,9 +566,10 @@ mixin NanoInterface on Bip39Wallet { final data = await _fetchAll(publicAddress, null, null); - final transactions = data["history"] is List - ? data["history"] as List - : []; + final transactions = + data["history"] is List + ? data["history"] as List + : []; if (transactions.isEmpty) { return; } else { @@ -612,17 +607,18 @@ mixin NanoInterface on Bip39Wallet { numberOfMessages: null, ); - final Address address = transactionType == TransactionType.incoming - ? receivingAddress - : Address( - walletId: walletId, - publicKey: [], - value: tx["account"].toString(), - derivationIndex: 0, - derivationPath: null, - type: info.mainAddressType, - subType: AddressSubType.nonWallet, - ); + final Address address = + transactionType == TransactionType.incoming + ? receivingAddress + : Address( + walletId: walletId, + publicKey: [], + value: tx["account"].toString(), + derivationIndex: 0, + derivationPath: null, + type: info.mainAddressType, + subType: AddressSubType.nonWallet, + ); final Tuple2 tuple = Tuple2(transaction, address); transactionList.add(tuple); } @@ -653,8 +649,9 @@ mixin NanoInterface on Bip39Wallet { final data = jsonDecode(response.body); final balance = Balance( total: Amount( - rawValue: (BigInt.parse(data["balance"].toString()) + - BigInt.parse(data["receivable"].toString())), + rawValue: + (BigInt.parse(data["balance"].toString()) + + BigInt.parse(data["receivable"].toString())), fractionDigits: cryptoCurrency.fractionDigits, ), spendable: Amount( @@ -703,10 +700,8 @@ mixin NanoInterface on Bip39Wallet { ); final infoData = jsonDecode(infoResponse.body); - final height = int.tryParse( - infoData["confirmation_height"].toString(), - ) ?? - 0; + final height = + int.tryParse(infoData["confirmation_height"].toString()) ?? 0; await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } catch (e, s) { @@ -734,21 +729,21 @@ mixin NanoInterface on Bip39Wallet { @override // nano has no fees - Future estimateFeeFor(Amount amount, int feeRate) async => Amount( - rawValue: BigInt.from(0), - fractionDigits: cryptoCurrency.fractionDigits, - ); + Future estimateFeeFor(Amount amount, BigInt feeRate) async => Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); @override // nano has no fees Future get fees async => FeeObject( - numberOfBlocksFast: 1, - numberOfBlocksAverage: 1, - numberOfBlocksSlow: 1, - fast: 0, - medium: 0, - slow: 0, - ); + numberOfBlocksFast: 1, + numberOfBlocksAverage: 1, + numberOfBlocksSlow: 1, + fast: BigInt.zero, + medium: BigInt.zero, + slow: BigInt.zero, + ); void _hackedCheckTorNodePrefs() { final node = getCurrentNode(); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 5c0ace879..1bfb3a2a3 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -457,7 +457,7 @@ mixin PaynymInterface } Future prepareNotificationTx({ - required int selectedTxFeeRate, + required BigInt selectedTxFeeRate, required String targetPaymentCodeString, int additionalOutputs = 0, List? utxos, diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 347bf9f2c..c22429d10 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,88 @@ 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 { + // TODO remove after block 1104500 + if (cryptoCurrency.network == CryptoCurrencyNetwork.main) { + final height = await fetchChainHeight(); + if (height < 1104500) { + throw Exception("Spark names not enabled on main net yet"); + } + } + + 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 +2257,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 +2332,7 @@ Future< allAnonymitySets: args.allAnonymitySets, idAndBlockHashes: args.idAndBlockHashes, txHash: args.txHash, + additionalTxSize: args.additionalTxSize, ); return spend; @@ -2111,7 +2346,8 @@ Future> _identifyCoins( Set privateKeyHexSet, String walletId, bool isTestNet, - }) args, + }) + args, ) async { final List myCoins = []; @@ -2200,12 +2436,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 +2451,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 +2474,10 @@ int _estSparkFeeComputeFunc( bool subtractFeeFromAmount, List serializedCoins, int privateRecipientsCount, - }) args, + int utxoNum, + int additionalTxSize, + }) + args, ) { final est = LibSpark.estimateSparkFee( privateKeyHex: args.privateKeyHex, @@ -2245,6 +2486,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/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 713aaa109..aa3175cac 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -4,8 +4,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/models.dart'; import '../../pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; -import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; import '../../providers/global/wallets_provider.dart'; +import '../../providers/ui/fee_rate_type_state_provider.dart'; +import '../../providers/wallet/desktop_fee_providers.dart'; import '../../providers/wallet/public_private_balance_state_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; @@ -43,7 +44,7 @@ class _DesktopFeeDialogState extends ConsumerState { Future feeFor({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, }) async { switch (feeRateType) { @@ -62,26 +63,30 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.high.value, + BigInt.from(lib_monero.TransactionPriority.high.value), ); ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -112,21 +117,25 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.medium.value, + BigInt.from(lib_monero.TransactionPriority.medium.value), ); ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else { @@ -162,26 +171,30 @@ class _DesktopFeeDialogState extends ConsumerState { if (coin is Monero || coin is Wownero) { final fee = await wallet.estimateFeeFor( amount, - lib_monero.TransactionPriority.normal.value, + BigInt.from(lib_monero.TransactionPriority.normal.value), ); ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { case FiroType.spark: - fee = - await (wallet as FiroWallet).estimateFeeForSpark(amount); + fee = await (wallet as FiroWallet).estimateFeeForSpark( + amount, + ); case FiroType.lelantus: - fee = await (wallet as FiroWallet) - .estimateFeeForLelantus(amount); + fee = await (wallet as FiroWallet).estimateFeeForLelantus( + amount, + ); case FiroType.public: - fee = await (wallet as FiroWallet) - .estimateFeeFor(amount, feeRate); + fee = await (wallet as FiroWallet).estimateFeeFor( + amount, + feeRate, + ); } ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - await wallet.estimateFeeFor(amount, feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = await wallet + .estimateFeeFor(amount, feeRate); } } else { final tokenWallet = ref.read(pCurrentTokenWallet)!; @@ -214,9 +227,7 @@ class _DesktopFeeDialogState extends ConsumerState { maxHeight: double.infinity, child: FutureBuilder( future: ref.watch( - pWallets.select( - (value) => value.getWallet(walletId).fees, - ), + pWallets.select((value) => value.getWallet(walletId).fees), ), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -255,9 +266,7 @@ class _DesktopFeeDialogState extends ConsumerState { ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ); }, @@ -283,9 +292,10 @@ class DesktopFeeItem extends ConsumerStatefulWidget { final Future Function({ required Amount amount, required FeeRateType feeRateType, - required int feeRate, + required BigInt feeRate, required CryptoCurrency coin, - }) feeFor; + }) + feeFor; final bool isSelected; final bool isButton; @@ -337,19 +347,18 @@ class _DesktopFeeItemState extends ConsumerState { return ConditionalParent( condition: widget.isButton, - builder: (child) => MaterialButton( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pop( - ( - widget.feeRateType, - feeString, - timeString, - ), - ); - }, - child: child, - ), + builder: + (child) => MaterialButton( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + ref.read(feeRateTypeDesktopStateProvider.state).state = + widget.feeRateType; + Navigator.of( + context, + ).pop((widget.feeRateType, feeString, timeString)); + }, + child: child, + ), child: Builder( builder: (_) { if (!widget.isButton) { @@ -360,19 +369,14 @@ class _DesktopFeeItemState extends ConsumerState { ); if ((coin is Firo) && ref.watch(publicPrivateBalanceStateProvider.state).state == - "Private") { + FiroType.lelantus) { return Text( - "~${ref.watch(pAmountFormatter(coin)).format( - Amount( - rawValue: BigInt.parse("3794"), - fractionDigits: coin.fractionDigits, - ), - indicatePrecisionLoss: false, - )}", + "~${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, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), textAlign: TextAlign.left, ); @@ -385,11 +389,13 @@ class _DesktopFeeItemState extends ConsumerState { children: [ Text( widget.feeRateType.prettyName, - style: - STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), textAlign: TextAlign.left, ), @@ -405,9 +411,10 @@ class _DesktopFeeItemState extends ConsumerState { return AnimatedText( stringsToLoopThrough: stringsToLoopThrough, style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), ); } else { @@ -415,9 +422,10 @@ class _DesktopFeeItemState extends ConsumerState { future: widget.feeFor( coin: wallet.info.coin, feeRateType: widget.feeRateType, - feeRate: widget.feeRateType == FeeRateType.fast - ? widget.feeObject!.fast - : widget.feeRateType == FeeRateType.slow + feeRate: + widget.feeRateType == FeeRateType.fast + ? widget.feeObject!.fast + : widget.feeRateType == FeeRateType.slow ? widget.feeObject!.slow : widget.feeObject!.medium, amount: ref.watch(sendAmountProvider.state).state, @@ -425,44 +433,47 @@ class _DesktopFeeItemState extends ConsumerState { builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { - feeString = "${widget.feeRateType.prettyName} " - "(~${ref.watch(pAmountFormatter(wallet.info.coin)).format( - snapshot.data!, - indicatePrecisionLoss: false, - )})"; - - timeString = wallet.info.coin is Ethereum - ? "" - : estimatedTimeToBeIncludedInNextBlock( - wallet.info.coin.targetBlockTimeSeconds, - widget.feeRateType == FeeRateType.fast - ? widget.feeObject!.numberOfBlocksFast - : widget.feeRateType == FeeRateType.slow - ? widget.feeObject!.numberOfBlocksSlow - : widget.feeObject!.numberOfBlocksAverage, - ); + feeString = + "${widget.feeRateType.prettyName} " + "(~${ref.watch(pAmountFormatter(wallet.info.coin)).format(snapshot.data!, indicatePrecisionLoss: false)})"; + + timeString = + wallet.info.coin is Ethereum + ? "" + : estimatedTimeToBeIncludedInNextBlock( + wallet.info.coin.targetBlockTimeSeconds, + widget.feeRateType == FeeRateType.fast + ? widget.feeObject!.numberOfBlocksFast + : widget.feeRateType == FeeRateType.slow + ? widget.feeObject!.numberOfBlocksSlow + : widget.feeObject!.numberOfBlocksAverage, + ); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( feeString!, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), textAlign: TextAlign.left, ), if (widget.feeObject != null) Text( timeString!, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, ), ), ], @@ -470,11 +481,13 @@ class _DesktopFeeItemState extends ConsumerState { } else { return AnimatedText( stringsToLoopThrough: stringsToLoopThrough, - style: STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveText, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, ), ); } diff --git a/lib/widgets/eth_fee_form.dart b/lib/widgets/eth_fee_form.dart new file mode 100644 index 000000000..2f4e2889e --- /dev/null +++ b/lib/widgets/eth_fee_form.dart @@ -0,0 +1,310 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; + +import '../services/ethereum/ethereum_api.dart'; +import '../themes/stack_colors.dart'; +import '../utilities/constants.dart'; +import '../utilities/text_styles.dart'; +import '../utilities/util.dart'; +import 'stack_text_field.dart'; + +@immutable +class EthEIP1559Fee { + final Decimal maxBaseFeeGwei; + final Decimal priorityFeeGwei; + final int gasLimit; + + const EthEIP1559Fee({ + required this.maxBaseFeeGwei, + required this.priorityFeeGwei, + required this.gasLimit, + }); + + BigInt get maxBaseFeeWei => maxBaseFeeGwei.shift(9).toBigInt(); + BigInt get priorityFeeWei => priorityFeeGwei.shift(9).toBigInt(); + + @override + String toString() => + "EthEIP1559Fee(" + "maxBaseFeeGwei: $maxBaseFeeGwei, " + "priorityFeeGwei: $priorityFeeGwei, " + "maxBaseFeeWei: $maxBaseFeeWei, " + "priorityFeeWei: $priorityFeeWei, " + "gasLimit: $gasLimit)"; +} + +class EthFeeForm extends StatefulWidget { + EthFeeForm({ + super.key, + this.minGasLimit = 21000, + this.maxGasLimit = 30000000, + this.initialState, + required this.stateChanged, + }) : assert( + initialState == null || + (initialState.gasLimit >= minGasLimit && + initialState.gasLimit <= maxGasLimit), + ); + + final int minGasLimit; + final int maxGasLimit; + + final EthEIP1559Fee? initialState; + + final void Function(EthEIP1559Fee) stateChanged; + + @override + State createState() => _EthFeeFormState(); +} + +class _EthFeeFormState extends State { + static const _textFadeDuration = Duration(milliseconds: 300); + + final maxBaseController = TextEditingController(); + final priorityFeeController = TextEditingController(); + final gasLimitController = TextEditingController(); + final maxBaseFocus = FocusNode(); + final priorityFeeFocus = FocusNode(); + final gasLimitFocus = FocusNode(); + + late int _gasLimitCache; + + EthEIP1559Fee get _current => EthEIP1559Fee( + maxBaseFeeGwei: Decimal.tryParse(maxBaseController.text) ?? Decimal.zero, + priorityFeeGwei: + Decimal.tryParse(priorityFeeController.text) ?? Decimal.zero, + gasLimit: int.parse(gasLimitController.text), + ); + + String _currentBase = "Current: "; + String _currentPriority = "Current: "; + + void _checkNetworkGas() async { + final gas = await EthereumAPI.getGasOracle(); + + if (mounted) { + setState(() { + _currentBase = + "Current: ${gas.value!.suggestBaseFee.toStringAsFixed(3)} GWEI"; + _currentPriority = + "Current: ${gas.value!.lowPriority.toStringAsFixed(3)} - ${gas.value!.highPriority.toStringAsFixed(3)} GWEI"; + }); + } + } + + Timer? _gasTimer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkNetworkGas(); + _gasTimer = Timer.periodic( + const Duration(seconds: 5), + (_) => _checkNetworkGas(), + ); + }); + + maxBaseController.text = + widget.initialState?.maxBaseFeeGwei.toString() ?? ""; + priorityFeeController.text = + widget.initialState?.priorityFeeGwei.toString() ?? ""; + + _gasLimitCache = widget.initialState?.gasLimit ?? widget.minGasLimit; + gasLimitController.text = _gasLimitCache.toString(); + } + + @override + void dispose() { + _gasTimer?.cancel(); + _gasTimer = null; + maxBaseController.dispose(); + priorityFeeController.dispose(); + gasLimitController.dispose(); + maxBaseFocus.dispose(); + priorityFeeFocus.dispose(); + gasLimitFocus.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Max base fee (GWEI)", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: maxBaseController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: maxBaseFocus, + onChanged: (value) { + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + maxBaseFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + const SizedBox(height: 6), + AnimatedSwitcher( + duration: _textFadeDuration, + transitionBuilder: + (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + _currentBase, + key: ValueKey( + _currentBase, + ), // Important: ensures AnimatedSwitcher sees the text change + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox(height: 20), + Text("Priority fee (GWEI)", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: priorityFeeController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: priorityFeeFocus, + onChanged: (value) { + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + priorityFeeFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + const SizedBox(height: 6), + AnimatedSwitcher( + duration: _textFadeDuration, + transitionBuilder: + (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + _currentPriority, + key: ValueKey( + _currentPriority, + ), // Important: ensures AnimatedSwitcher sees the text change + style: STextStyles.smallMed12(context), + ), + ), + const SizedBox(height: 20), + Text("Gas limit", style: STextStyles.smallMed12(context)), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 1, + controller: gasLimitController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + focusNode: gasLimitFocus, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue == null || + intValue < widget.minGasLimit || + intValue > widget.maxGasLimit) { + gasLimitController.text = _gasLimitCache.toString(); + return; + } + + _gasLimitCache = intValue; + + widget.stateChanged(_current); + }, + style: + Util.isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + null, + gasLimitFocus, + context, + desktopMed: Util.isDesktop, + ).copyWith( + contentPadding: EdgeInsets.only( + left: 16, + top: Util.isDesktop ? 11 : 6, + bottom: Util.isDesktop ? 12 : 8, + right: 5, + ), + ), + ), + ), + ], + ); + } +} 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/lib/widgets/trade_card.dart b/lib/widgets/trade_card.dart index 28a05f9ae..35c8e4578 100644 --- a/lib/widgets/trade_card.dart +++ b/lib/widgets/trade_card.dart @@ -15,7 +15,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import '../models/exchange/change_now/exchange_transaction_status.dart'; +import '../models/exchange/change_now/cn_exchange_transaction_status.dart'; import '../models/exchange/response_objects/trade.dart'; import '../models/isar/stack_theme.dart'; import '../themes/theme_providers.dart'; @@ -26,11 +26,7 @@ import 'conditional_parent.dart'; import 'rounded_white_container.dart'; class TradeCard extends ConsumerWidget { - const TradeCard({ - super.key, - required this.trade, - required this.onTap, - }); + const TradeCard({super.key, required this.trade, required this.onTap}); final Trade trade; final VoidCallback onTap; @@ -78,10 +74,9 @@ class TradeCard extends ConsumerWidget { return ConditionalParent( condition: isDesktop, - builder: (child) => MouseRegion( - cursor: SystemMouseCursors.click, - child: child, - ), + builder: + (child) => + MouseRegion(cursor: SystemMouseCursors.click, child: child), child: GestureDetector( onTap: onTap, child: RoundedWhiteContainer( @@ -108,9 +103,7 @@ class TradeCard extends ConsumerWidget { ), ), ), - const SizedBox( - width: 12, - ), + const SizedBox(width: 12), Expanded( child: Column( children: [ @@ -127,9 +120,7 @@ class TradeCard extends ConsumerWidget { ), ], ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index cab5043d6..f4227005e 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -143,30 +143,35 @@ class _TransactionCardState extends ConsumerState { localeServiceChangeNotifierProvider.select((value) => value.locale), ); - final baseCurrency = ref - .watch(prefsChangeNotifierProvider.select((value) => value.currency)); + final baseCurrency = ref.watch( + prefsChangeNotifierProvider.select((value) => value.currency), + ); - final price = ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => isTokenTx - ? value.getTokenPrice(_transaction.otherData!) - : value.getPrice(coin), - ), - ) - .item1; + final price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => + isTokenTx + ? value.getTokenPrice(_transaction.otherData!) + : value.getPrice(coin), + ), + ) + ?.value; final currentHeight = ref.watch( - pWallets - .select((value) => value.getWallet(walletId).info.cachedChainHeight), + pWallets.select( + (value) => value.getWallet(walletId).info.cachedChainHeight, + ), ); return Material( color: Theme.of(context).extension()!.popupBG, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), child: Padding( padding: const EdgeInsets.all(6), @@ -191,25 +196,22 @@ class _TransactionCardState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: TransactionDetailsView( - transaction: _transaction, - coin: coin, - walletId: walletId, - ), - ), + builder: + (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: TransactionDetailsView( + transaction: _transaction, + coin: coin, + walletId: walletId, + ), + ), ); } else { unawaited( Navigator.of(context).pushNamed( TransactionDetailsView.routeName, - arguments: Tuple3( - _transaction, - coin, - walletId, - ), + arguments: Tuple3(_transaction, coin, walletId), ), ); } @@ -223,9 +225,7 @@ class _TransactionCardState extends ConsumerState { coin: coin, currentHeight: currentHeight, ), - const SizedBox( - width: 14, - ), + const SizedBox(width: 14), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -243,17 +243,15 @@ class _TransactionCardState extends ConsumerState { ? "Failed" : "Cancelled" : whatIsIt( - _transaction.type, - coin, - currentHeight, - ), + _transaction.type, + coin, + currentHeight, + ), style: STextStyles.itemSubtitle12(context), ), ), ), - const SizedBox( - width: 10, - ), + const SizedBox(width: 10), Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -271,9 +269,7 @@ class _TransactionCardState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, // crossAxisAlignment: CrossAxisAlignment.end, @@ -287,17 +283,19 @@ class _TransactionCardState extends ConsumerState { ), ), ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) - const SizedBox( - width: 10, - ), - if (ref.watch( - prefsChangeNotifierProvider - .select((value) => value.externalCalls), - )) + if (price != null && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + )) + const SizedBox(width: 10), + if (price != null && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + )) Flexible( child: FittedBox( fit: BoxFit.scaleDown, @@ -306,12 +304,7 @@ class _TransactionCardState extends ConsumerState { final amount = _transaction.realAmount; return Text( - "$prefix${Amount.fromDecimal( - amount.decimal * price, - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", + "$prefix${Amount.fromDecimal(amount.decimal * price, fractionDigits: 2).fiatString(locale: locale)} $baseCurrency", style: STextStyles.label(context), ); }, diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index 0212d89d6..74d91b33f 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; + import '../../../models/isar/exchange_cache/currency.dart'; import '../../../services/exchange/change_now/change_now_exchange.dart'; import '../../../services/exchange/exchange_data_loading_service.dart'; @@ -38,17 +39,15 @@ class WalletInfoCoinIcon extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { Currency? currency; if (contractAddress != null) { - currency = ExchangeDataLoadingService.instance.isar.currencies - .where() - .exchangeNameEqualTo(ChangeNowExchange.exchangeName) - .filter() - .tokenContractEqualTo( - contractAddress!, - caseSensitive: false, - ) - .and() - .imageIsNotEmpty() - .findFirstSync(); + currency = + ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo(contractAddress!, caseSensitive: false) + .and() + .imageIsNotEmpty() + .findFirstSync(); } return Container( @@ -62,19 +61,14 @@ class WalletInfoCoinIcon extends ConsumerWidget { ), child: Padding( padding: EdgeInsets.all(size / 5), - child: currency != null && currency.image.isNotEmpty - ? SvgPicture.network( - currency.image, - width: 20, - height: 20, - ) - : SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + child: + currency != null && currency.image.isNotEmpty + ? SvgPicture.network(currency.image, width: 20, height: 20) + : SvgPicture.file( + File(ref.watch(coinIconProvider(coin))), + width: 20, + height: 20, ), - width: 20, - height: 20, - ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 9d65621e5..16e439300 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: f5fd2238fca4ffe82f7e14646a613a04d2c243d6 + resolved-ref: f5fd2238fca4ffe82f7e14646a613a04d2c243d6 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..d086b0f94 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: f5fd2238fca4ffe82f7e14646a613a04d2c243d6 # 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/models/fee_object_model_test.dart b/test/models/fee_object_model_test.dart index 52045d662..1f108d531 100644 --- a/test/models/fee_object_model_test.dart +++ b/test/models/fee_object_model_test.dart @@ -4,27 +4,16 @@ import 'package:stackwallet/models/models.dart'; void main() { test("FeeObject constructor", () { final feeObject = FeeObject( - fast: 3, - medium: 2, - slow: 1, + fast: BigInt.from(3), + medium: BigInt.from(2), + slow: BigInt.from(1), numberOfBlocksFast: 4, numberOfBlocksSlow: 5, numberOfBlocksAverage: 10, ); - expect(feeObject.toString(), - "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 10, numberOfBlocksSlow: 5}"); - }); - - test("FeeObject.fromJson factory", () { - final feeObject = FeeObject.fromJson({ - "fast": 3, - "average": 2, - "slow": 1, - "numberOfBlocksFast": 4, - "numberOfBlocksSlow": 5, - "numberOfBlocksAverage": 6, - }); - expect(feeObject.toString(), - "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 6, numberOfBlocksSlow: 5}"); + expect( + feeObject.toString(), + "{fast: 3, medium: 2, slow: 1, numberOfBlocksFast: 4, numberOfBlocksAverage: 10, numberOfBlocksSlow: 5}", + ); }); } diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index 565f05f82..1271b96b0 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -10,22 +10,17 @@ import 'package:decimal/decimal.dart' as _i19; import 'package:logger/logger.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i7; -import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart' +import 'package:stackwallet/models/exchange/change_now/cn_exchange_transaction.dart' as _i22; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart' - as _i24; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' - as _i25; +import 'package:stackwallet/models/exchange/change_now/cn_exchange_transaction_status.dart' + as _i23; import 'package:stackwallet/models/exchange/response_objects/estimate.dart' as _i21; -import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart' - as _i23; import 'package:stackwallet/models/exchange/response_objects/range.dart' as _i20; import 'package:stackwallet/models/exchange/response_objects/trade.dart' as _i15; import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i18; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i26; import 'package:stackwallet/networking/http.dart' as _i3; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart' as _i17; @@ -1031,16 +1026,22 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse>> getAvailableCurrencies({ - bool? fixedRate, bool? active, + bool? buy, + bool? sell, + _i17.CNFlow? flow = _i17.CNFlow.standard, + String? apiKey, }) => (super.noSuchMethod( Invocation.method( #getAvailableCurrencies, [], { - #fixedRate: fixedRate, #active: active, + #buy: buy, + #sell: sell, + #flow: flow, + #apiKey: apiKey, }, ), returnValue: @@ -1051,64 +1052,23 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getAvailableCurrencies, [], { - #fixedRate: fixedRate, #active: active, + #buy: buy, + #sell: sell, + #flow: flow, + #apiKey: apiKey, }, ), )), ) as _i10.Future<_i4.ExchangeResponse>>); - @override - _i10.Future<_i4.ExchangeResponse>> getCurrenciesV2() => - (super.noSuchMethod( - Invocation.method( - #getCurrenciesV2, - [], - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getCurrenciesV2, - [], - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - - @override - _i10.Future<_i4.ExchangeResponse>> getPairedCurrencies({ - required String? ticker, - bool? fixedRate, - }) => - (super.noSuchMethod( - Invocation.method( - #getPairedCurrencies, - [], - { - #ticker: ticker, - #fixedRate: fixedRate, - }, - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getPairedCurrencies, - [], - { - #ticker: ticker, - #fixedRate: fixedRate, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - @override _i10.Future<_i4.ExchangeResponse<_i19.Decimal>> getMinimalExchangeAmount({ - required String? fromTicker, - required String? toTicker, + required String? fromCurrency, + required String? toCurrency, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1116,8 +1076,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getMinimalExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1128,8 +1091,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getMinimalExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1138,9 +1104,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse<_i20.Range>> getRange({ - required String? fromTicker, - required String? toTicker, - required bool? isFixedRate, + required String? fromCurrency, + required String? toCurrency, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1148,9 +1116,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getRange, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #isFixedRate: isFixedRate, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1161,9 +1131,11 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getRange, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #isFixedRate: isFixedRate, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, #apiKey: apiKey, }, ), @@ -1172,9 +1144,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { @override _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmount({ - required String? fromTicker, - required String? toTicker, - required _i19.Decimal? fromAmount, + required String? fromCurrency, + required String? toCurrency, + _i19.Decimal? fromAmount, + _i19.Decimal? toAmount, + String? fromNetwork, + String? toNetwork, + _i17.CNFlow? flow = _i17.CNFlow.standard, + _i17.CNExchangeType? type = _i17.CNExchangeType.direct, + bool? useRateId, + bool? isTopUp, String? apiKey, }) => (super.noSuchMethod( @@ -1182,9 +1161,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getEstimatedExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, #fromAmount: fromAmount, + #toAmount: toAmount, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, + #type: type, + #useRateId: useRateId, + #isTopUp: isTopUp, #apiKey: apiKey, }, ), @@ -1195,9 +1181,16 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { #getEstimatedExchangeAmount, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, + #fromCurrency: fromCurrency, + #toCurrency: toCurrency, #fromAmount: fromAmount, + #toAmount: toAmount, + #fromNetwork: fromNetwork, + #toNetwork: toNetwork, + #flow: flow, + #type: type, + #useRateId: useRateId, + #isTopUp: isTopUp, #apiKey: apiKey, }, ), @@ -1205,277 +1198,109 @@ class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> - getEstimatedExchangeAmountFixedRate({ - required String? fromTicker, - required String? toTicker, - required _i19.Decimal? fromAmount, - required bool? reversed, - bool? useRateId = true, - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #getEstimatedExchangeAmountFixedRate, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromAmount: fromAmount, - #reversed: reversed, - #useRateId: useRateId, - #apiKey: apiKey, - }, - ), - returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( - _FakeExchangeResponse_2<_i21.Estimate>( - this, - Invocation.method( - #getEstimatedExchangeAmountFixedRate, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromAmount: fromAmount, - #reversed: reversed, - #useRateId: useRateId, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); - - @override - _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>> - getEstimatedExchangeAmountV2({ - required String? fromTicker, - required String? toTicker, - required _i22.CNEstimateType? fromOrTo, - required _i19.Decimal? amount, - String? fromNetwork, - String? toNetwork, - _i22.CNFlowType? flow = _i22.CNFlowType.standard, + _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>> + createExchangeTransaction({ + required String? fromCurrency, + required String? fromNetwork, + required String? toCurrency, + required String? toNetwork, + _i19.Decimal? fromAmount, + _i19.Decimal? toAmount, + _i17.CNFlow? flow = _i17.CNFlow.standard, + _i17.CNExchangeType? type = _i17.CNExchangeType.direct, + required String? address, + String? extraId, + String? refundAddress, + String? refundExtraId, + String? userId, + String? payload, + String? contactEmail, + required String? rateId, String? apiKey, }) => (super.noSuchMethod( Invocation.method( - #getEstimatedExchangeAmountV2, + #createExchangeTransaction, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromOrTo: fromOrTo, - #amount: amount, + #fromCurrency: fromCurrency, #fromNetwork: fromNetwork, + #toCurrency: toCurrency, #toNetwork: toNetwork, + #fromAmount: fromAmount, + #toAmount: toAmount, #flow: flow, - #apiKey: apiKey, - }, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>.value( - _FakeExchangeResponse_2<_i22.CNExchangeEstimate>( - this, - Invocation.method( - #getEstimatedExchangeAmountV2, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #fromOrTo: fromOrTo, - #amount: amount, - #fromNetwork: fromNetwork, - #toNetwork: toNetwork, - #flow: flow, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>); - - @override - _i10.Future<_i4.ExchangeResponse>> - getAvailableFixedRateMarkets({String? apiKey}) => (super.noSuchMethod( - Invocation.method( - #getAvailableFixedRateMarkets, - [], - {#apiKey: apiKey}, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableFixedRateMarkets, - [], - {#apiKey: apiKey}, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); - - @override - _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> - createStandardExchangeTransaction({ - required String? fromTicker, - required String? toTicker, - required String? receivingAddress, - required _i19.Decimal? amount, - String? extraId = r'', - String? userId = r'', - String? contactEmail = r'', - String? refundAddress = r'', - String? refundExtraId = r'', - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #createStandardExchangeTransaction, - [], - { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, + #type: type, + #address: address, #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, #refundAddress: refundAddress, #refundExtraId: refundExtraId, + #userId: userId, + #payload: payload, + #contactEmail: contactEmail, + #rateId: rateId, #apiKey: apiKey, }, ), returnValue: _i10 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransaction>( + .Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i22.CNExchangeTransaction>( this, Invocation.method( - #createStandardExchangeTransaction, + #createExchangeTransaction, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, + #fromCurrency: fromCurrency, + #fromNetwork: fromNetwork, + #toCurrency: toCurrency, + #toNetwork: toNetwork, + #fromAmount: fromAmount, + #toAmount: toAmount, + #flow: flow, + #type: type, + #address: address, #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, #refundAddress: refundAddress, #refundExtraId: refundExtraId, + #userId: userId, + #payload: payload, + #contactEmail: contactEmail, + #rateId: rateId, #apiKey: apiKey, }, ), )), - ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeTransaction>>); @override - _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> - createFixedRateExchangeTransaction({ - required String? fromTicker, - required String? toTicker, - required String? receivingAddress, - required _i19.Decimal? amount, - required String? rateId, - required bool? reversed, - String? extraId = r'', - String? userId = r'', - String? contactEmail = r'', - String? refundAddress = r'', - String? refundExtraId = r'', + _i10.Future<_i4.ExchangeResponse<_i23.CNExchangeTransactionStatus>> + getTransactionStatus({ + required String? id, String? apiKey, }) => (super.noSuchMethod( Invocation.method( - #createFixedRateExchangeTransaction, + #getTransactionStatus, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, - #rateId: rateId, - #reversed: reversed, - #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, - #refundAddress: refundAddress, - #refundExtraId: refundExtraId, + #id: id, #apiKey: apiKey, }, ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransaction>( + returnValue: _i10.Future< + _i4 + .ExchangeResponse<_i23.CNExchangeTransactionStatus>>.value( + _FakeExchangeResponse_2<_i23.CNExchangeTransactionStatus>( this, Invocation.method( - #createFixedRateExchangeTransaction, + #getTransactionStatus, [], { - #fromTicker: fromTicker, - #toTicker: toTicker, - #receivingAddress: receivingAddress, - #amount: amount, - #rateId: rateId, - #reversed: reversed, - #extraId: extraId, - #userId: userId, - #contactEmail: contactEmail, - #refundAddress: refundAddress, - #refundExtraId: refundExtraId, + #id: id, #apiKey: apiKey, }, ), )), - ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); - - @override - _i10.Future< - _i4 - .ExchangeResponse<_i25.ExchangeTransactionStatus>> getTransactionStatus({ - required String? id, - String? apiKey, - }) => - (super.noSuchMethod( - Invocation.method( - #getTransactionStatus, - [], - { - #id: id, - #apiKey: apiKey, - }, - ), - returnValue: _i10 - .Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>.value( - _FakeExchangeResponse_2<_i25.ExchangeTransactionStatus>( - this, - Invocation.method( - #getTransactionStatus, - [], - { - #id: id, - #apiKey: apiKey, - }, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>); - - @override - _i10.Future<_i4.ExchangeResponse>> - getAvailableFloatingRatePairs({bool? includePartners = false}) => - (super.noSuchMethod( - Invocation.method( - #getAvailableFloatingRatePairs, - [], - {#includePartners: includePartners}, - ), - returnValue: - _i10.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableFloatingRatePairs, - [], - {#includePartners: includePartners}, - ), - )), - ) as _i10.Future<_i4.ExchangeResponse>>); + ) as _i10 + .Future<_i4.ExchangeResponse<_i23.CNExchangeTransactionStatus>>); } diff --git a/test/services/change_now/change_now_test.dart b/test/services/change_now/change_now_test.dart index f49c52064..f922bc3ed 100644 --- a/test/services/change_now/change_now_test.dart +++ b/test/services/change_now/change_now_test.dart @@ -8,7 +8,6 @@ import 'package:stackwallet/exceptions/exchange/exchange_exception.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; import 'package:stackwallet/networking/http.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart'; @@ -23,12 +22,16 @@ void main() { final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(availableCurrenciesJSON)), 200)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => + Response(utf8.encode(jsonEncode(availableCurrenciesJSON)), 200), + ); final result = await instance.getAvailableCurrencies(); @@ -41,12 +44,18 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies?active=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONActive)), 200)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies?active=true"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONActive)), + 200, + ), + ); final result = await instance.getAvailableCurrencies(active: true); @@ -59,36 +68,24 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies?fixedRate=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONFixedRate)), 200)); - - final result = await instance.getAvailableCurrencies(fixedRate: true); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 410); - }); - - test("getAvailableCurrencies succeeds with fixedRate and active options", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/currencies?fixedRate=true&active=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(availableCurrenciesJSONActiveFixedRate)), - 200)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/currencies?fixedRate=true", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONFixedRate)), + 200, + ), + ); - final result = - await instance.getAvailableCurrencies(active: true, fixedRate: true); + final result = await instance.getAvailableCurrencies( + flow: CNFlow.fixedRate, + ); expect(result.exception, null); expect(result.value == null, false); @@ -96,116 +93,84 @@ void main() { }); test( - "getAvailableCurrencies fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('{"some unexpected": "but valid json data"}'), 200)); - - final result = await instance.getAvailableCurrencies(); + "getAvailableCurrencies succeeds with fixedRate and active options", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/currencies?fixedRate=true&active=true", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(availableCurrenciesJSONActiveFixedRate)), + 200, + ), + ); + + final result = await instance.getAvailableCurrencies( + active: true, + flow: CNFlow.fixedRate, + ); + + expect(result.exception, null); + expect(result.value == null, false); + expect(result.value!.length, 410); + }, + ); - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + test( + "getAvailableCurrencies fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode('{"some unexpected": "but valid json data"}'), + 200, + ), + ); + + final result = await instance.getAvailableCurrencies(); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getAvailableCurrencies fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); + when( + client.get( + url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); final result = await instance.getAvailableCurrencies(); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("getPairedCurrencies", () { - test("getPairedCurrencies succeeds without fixedRate option", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies-to/XMR"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(getPairedCurrenciesJSON)), 200)); - - final result = await instance.getPairedCurrencies(ticker: "XMR"); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 537); - }); - - test("getPairedCurrencies succeeds with fixedRate option", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/currencies-to/XMR?fixedRate=true"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(getPairedCurrenciesJSONFixedRate)), 200)); - - final result = - await instance.getPairedCurrencies(ticker: "XMR", fixedRate: true); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 410); - }); - - test( - "getPairedCurrencies fails with ChangeNowExceptionType.serializeResponseError A", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies-to/XMR"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('[{"some unexpected": "but valid json data"}]'), 200)); - - final result = await instance.getPairedCurrencies(ticker: "XMR"); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getPairedCurrencies fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse("https://api.ChangeNow.io/v1/currencies"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(""), 400)); - - final result = - await instance.getPairedCurrencies(ticker: "XMR", fixedRate: true); - - expect(result.exception!.type, ExchangeExceptionType.generic); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); @@ -215,17 +180,22 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"minAmount": 42}'), 200)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => + Response(utf8.encode('{"minAmount": 42}'), 200), + ); final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", apiKey: "testAPIKEY", ); @@ -235,49 +205,61 @@ void main() { }); test( - "getMinimalExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getMinimalExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getMinimalExchangeAmount( + fromCurrency: "xmr", + toCurrency: "btc", + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getMinimalExchangeAmount fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/min-amount/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getMinimalExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", apiKey: "testAPIKEY", ); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); @@ -287,19 +269,26 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"estimatedAmount": 58.4142873, "transactionSpeedForecast": "10-60", "warningMessage": null}'), - 200)); + '{"estimatedAmount": 58.4142873, "transactionSpeedForecast": "10-60", "warningMessage": null}', + ), + 200, + ), + ); final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", fromAmount: Decimal.fromInt(42), apiKey: "testAPIKEY", ); @@ -310,45 +299,55 @@ void main() { }); test( - "getEstimatedExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", - fromAmount: Decimal.fromInt(42), - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getEstimatedExchangeAmount fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getEstimatedExchangeAmount( + fromCurrency: "xmr", + toCurrency: "btc", + fromAmount: Decimal.fromInt(42), + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getEstimatedExchangeAmount fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/exchange-amount/42/xmr_btc?api_key=testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getEstimatedExchangeAmount( - fromTicker: "xmr", - toTicker: "btc", + fromCurrency: "xmr", + toCurrency: "btc", fromAmount: Decimal.fromInt(42), apiKey: "testAPIKEY", ); @@ -373,8 +372,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -400,8 +399,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -425,8 +424,8 @@ void main() { // // final result = // await ChangeNow.instance.getEstimatedFixedRateExchangeAmount( - // fromTicker: "xmr", - // toTicker: "btc", + // fromCurrency: "xmr", + // toCurrency: "btc", // fromAmount: Decimal.fromInt(10), // apiKey: "testAPIKEY", // ); @@ -436,95 +435,38 @@ void main() { // }); // }); - group("getAvailableFixedRateMarkets", () { - test("getAvailableFixedRateMarkets succeeds", () async { + group("createExchangeTransaction", () { + test("createExchangeTransaction succeeds", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode(jsonEncode(fixedRateMarketsJSON)), 200)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", + when( + client.post( + url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode(jsonEncode(createStandardTransactionResponse)), + 200, + ), ); - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value!.length, 237); - }); - - test( - "getAvailableFixedRateMarkets fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getAvailableFixedRateMarkets fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.getAvailableFixedRateMarkets( - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("createStandardExchangeTransaction", () { - test("createStandardExchangeTransaction succeeds", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode(jsonEncode(createStandardTransactionResponse)), 200)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', ); expect(result.exception, null); @@ -533,58 +475,73 @@ void main() { }); test( - "createStandardExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), - refundAddress: - "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("createStandardExchangeTransaction fails for any other reason", - () async { + "createExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), + refundAddress: + "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", + apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); + + test("createExchangeTransaction fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.createStandardExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + when( + client.post( + url: Uri.parse("https://api.ChangeNow.io/v1/transactions/testAPIKEY"), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"xmr","to":"btc","address":"bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5","amount":"0.3","flow":"standard","extraId":"","userId":"","contactEmail":"","refundAddress":"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H","refundExtraId":""}', + encoding: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", + fromNetwork: 'xmr', + toNetwork: '', + rateId: '', ); expect(result.exception!.type, ExchangeExceptionType.generic); @@ -592,37 +549,46 @@ void main() { }); }); - group("createFixedRateExchangeTransaction", () { - test("createFixedRateExchangeTransaction succeeds", () async { + group("createExchangeTransaction", () { + test("createExchangeTransaction succeeds", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":"","amount":"0.3"}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":"","amount":"0.3"}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"payinAddress": "33eFX2jfeWbXMSmRe9ewUUTrmSVSxZi5cj", "payoutAddress":' - ' "0x57f31ad4b64095347F87eDB1675566DAfF5EC886","payoutExtraId": "",' - ' "fromCurrency": "btc", "toCurrency": "eth", "refundAddress": "",' - '"refundExtraId": "","validUntil": "2019-09-09T14:01:04.921Z","id":' - ' "a5c73e2603f40d","amount": 62.9737711}'), - 200)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "btc", - toTicker: "eth", - receivingAddress: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", - amount: Decimal.parse("0.3"), + '{"payinAddress": "33eFX2jfeWbXMSmRe9ewUUTrmSVSxZi5cj", "payoutAddress":' + ' "0x57f31ad4b64095347F87eDB1675566DAfF5EC886","payoutExtraId": "",' + ' "fromCurrency": "btc", "toCurrency": "eth", "refundAddress": "",' + '"refundExtraId": "","validUntil": "2019-09-09T14:01:04.921Z","id":' + ' "a5c73e2603f40d","amount": 62.9737711}', + ), + 200, + ), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "btc", + toCurrency: "eth", + address: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", + fromAmount: Decimal.parse("0.3"), refundAddress: "", apiKey: "testAPIKEY", rateId: '', - reversed: false, + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', ); expect(result.exception, null); @@ -631,62 +597,76 @@ void main() { }); test( - "createFixedRateExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","amount":"0.3","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":""}', - encoding: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('{"id": "a5c73e2603f40d","amount": 62.9737711}'), 200)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "btc", - toTicker: "eth", - receivingAddress: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", - amount: Decimal.parse("0.3"), - refundAddress: "", - apiKey: "testAPIKEY", - rateId: '', - reversed: false, - ); - - expect(result.exception!.type, ExchangeExceptionType.generic); - expect(result.value == null, true); - }); - - test("createFixedRateExchangeTransaction fails for any other reason", - () async { + "createExchangeTransaction fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from":"btc","to":"eth","address":"0x57f31ad4b64095347F87eDB1675566DAfF5EC886","amount":"0.3","flow":"fixed-rate","extraId":"","userId":"","contactEmail":"","refundAddress":"","refundExtraId":"","rateId":""}', + encoding: null, + ), + ).thenAnswer( + (realInvocation) async => Response( + utf8.encode('{"id": "a5c73e2603f40d","amount": 62.9737711}'), + 200, + ), + ); + + final result = await instance.createExchangeTransaction( + fromCurrency: "btc", + toCurrency: "eth", + address: "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", + fromAmount: Decimal.parse("0.3"), + refundAddress: "", + apiKey: "testAPIKEY", + rateId: '', + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', + ); + + expect(result.exception!.type, ExchangeExceptionType.generic); + expect(result.value == null, true); + }, + ); + + test("createExchangeTransaction fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.post( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - body: - '{"from": "btc","to": "eth","address": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", "amount": "1.12345","extraId": "", "userId": "","contactEmail": "","refundAddress": "", "refundExtraId": "", "rateId": "" }', - encoding: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.createFixedRateExchangeTransaction( - fromTicker: "xmr", - toTicker: "btc", - receivingAddress: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", - amount: Decimal.parse("0.3"), + when( + client.post( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/fixed-rate/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + body: + '{"from": "btc","to": "eth","address": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", "amount": "1.12345","extraId": "", "userId": "","contactEmail": "","refundAddress": "", "refundExtraId": "", "rateId": "" }', + encoding: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + + final result = await instance.createExchangeTransaction( + fromCurrency: "xmr", + toCurrency: "btc", + address: "bc1qu58svs9983e2vuyqh7gq7ratf8k5qehz5k0cn5", + fromAmount: Decimal.parse("0.3"), refundAddress: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", apiKey: "testAPIKEY", rateId: '', - reversed: false, + type: CNExchangeType.direct, + fromNetwork: 'xmr', + toNetwork: '', ); expect(result.exception!.type, ExchangeExceptionType.generic); @@ -699,20 +679,27 @@ void main() { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response( utf8.encode( - '{"status": "waiting", "payinAddress": "32Ge2ci26rj1sRGw2NjiQa9L7Xvxtgzhrj", ' - '"payoutAddress": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", ' - '"fromCurrency": "btc", "toCurrency": "eth", "id": "50727663e5d9a4", ' - '"updatedAt": "2019-08-22T14:47:49.943Z", "expectedSendAmount": 1, ' - '"expectedReceiveAmount": 52.31667, "createdAt": "2019-08-22T14:47:49.943Z",' - ' "isPartner": false}'), - 200)); + '{"status": "waiting", "payinAddress": "32Ge2ci26rj1sRGw2NjiQa9L7Xvxtgzhrj", ' + '"payoutAddress": "0x57f31ad4b64095347F87eDB1675566DAfF5EC886", ' + '"fromCurrency": "btc", "toCurrency": "eth", "id": "50727663e5d9a4", ' + '"updatedAt": "2019-08-22T14:47:49.943Z", "expectedSendAmount": 1, ' + '"expectedReceiveAmount": 52.31667, "createdAt": "2019-08-22T14:47:49.943Z",' + ' "isPartner": false}', + ), + 200, + ), + ); final result = await instance.getTransactionStatus( id: "47F87eDB1675566DAfF5EC886", @@ -725,39 +712,49 @@ void main() { }); test( - "getTransactionStatus fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getTransactionStatus( - id: "47F87eDB1675566DAfF5EC886", - apiKey: "testAPIKEY", - ); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); + "getTransactionStatus fails with ChangeNowExceptionType.serializeResponseError", + () async { + final client = MockHTTP(); + final instance = ChangeNowAPI(http: client); + + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer( + (realInvocation) async => Response(utf8.encode('{"error": 42}'), 200), + ); + + final result = await instance.getTransactionStatus( + id: "47F87eDB1675566DAfF5EC886", + apiKey: "testAPIKEY", + ); + + expect( + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); + expect(result.value == null, true); + }, + ); test("getTransactionStatus fails for any other reason", () async { final client = MockHTTP(); final instance = ChangeNowAPI(http: client); - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); + when( + client.get( + url: Uri.parse( + "https://api.ChangeNow.io/v1/transactions/47F87eDB1675566DAfF5EC886/testAPIKEY", + ), + headers: {'Content-Type': 'application/json'}, + proxyInfo: null, + ), + ).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); final result = await instance.getTransactionStatus( id: "47F87eDB1675566DAfF5EC886", @@ -765,67 +762,9 @@ void main() { ); expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - }); - - group("getAvailableFloatingRatePairs", () { - test("getAvailableFloatingRatePairs succeeds", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response( - utf8.encode('["btc_xmr","btc_firo","btc_doge","eth_ltc"]'), 200)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect(result.exception, null); - expect(result.value == null, false); - expect(result.value, isA>()); - }); - - test( - "getAvailableFloatingRatePairs fails with ChangeNowExceptionType.serializeResponseError", - () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => - Response(utf8.encode('{"error": 42}'), 200)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); - expect(result.value == null, true); - }); - - test("getAvailableFloatingRatePairs fails for any other reason", () async { - final client = MockHTTP(); - final instance = ChangeNowAPI(http: client); - - when(client.get( - url: Uri.parse( - "https://api.ChangeNow.io/v1/market-info/available-pairs?includePartners=false"), - headers: {'Content-Type': 'application/json'}, - proxyInfo: null, - )).thenAnswer((realInvocation) async => Response(utf8.encode(''), 400)); - - final result = await instance.getAvailableFloatingRatePairs(); - - expect( - result.exception!.type, ExchangeExceptionType.serializeResponseError); + result.exception!.type, + ExchangeExceptionType.serializeResponseError, + ); expect(result.value == null, true); }); }); 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, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index a146a9abf..ac6790693 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -3,41 +3,41 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i11; -import 'dart:typed_data' as _i26; -import 'dart:ui' as _i17; +import 'dart:async' as _i10; +import 'dart:typed_data' as _i25; +import 'dart:ui' as _i16; -import 'package:decimal/decimal.dart' as _i23; -import 'package:isar/isar.dart' as _i9; -import 'package:logger/logger.dart' as _i20; +import 'package:decimal/decimal.dart' as _i22; +import 'package:isar/isar.dart' as _i8; +import 'package:logger/logger.dart' as _i19; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i16; +import 'package:mockito/src/dummies.dart' as _i15; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i28; +import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i27; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' as _i30; -import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i27; -import 'package:stackwallet/models/isar/models/isar_models.dart' as _i29; -import 'package:stackwallet/models/isar/stack_theme.dart' as _i25; -import 'package:stackwallet/networking/http.dart' as _i8; -import 'package:stackwallet/services/locale_service.dart' as _i15; +import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i26; +import 'package:stackwallet/models/isar/models/isar_models.dart' as _i28; +import 'package:stackwallet/models/isar/stack_theme.dart' as _i24; +import 'package:stackwallet/networking/http.dart' as _i7; +import 'package:stackwallet/services/locale_service.dart' as _i14; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/price_service.dart' as _i22; -import 'package:stackwallet/services/wallets.dart' as _i10; -import 'package:stackwallet/themes/theme_service.dart' as _i24; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i21; -import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i19; -import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i18; +import 'package:stackwallet/services/price_service.dart' as _i21; +import 'package:stackwallet/services/wallets.dart' as _i9; +import 'package:stackwallet/themes/theme_service.dart' as _i23; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; +import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i18; +import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i17; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' - as _i13; -import 'package:stackwallet/utilities/prefs.dart' as _i14; + as _i12; +import 'package:stackwallet/utilities/prefs.dart' as _i13; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' as _i4; -import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i12; +import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i11; import 'package:stackwallet/wallets/wallet/wallet.dart' as _i5; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i6; -import 'package:tuple/tuple.dart' as _i7; +import 'package:tuple/tuple.dart' as _i29; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -103,9 +103,8 @@ class _FakeDuration_4 extends _i1.SmartFake implements Duration { ); } -class _FakeTuple2_5 extends _i1.SmartFake - implements _i7.Tuple2 { - _FakeTuple2_5( +class _FakeHTTP_5 extends _i1.SmartFake implements _i7.HTTP { + _FakeHTTP_5( Object parent, Invocation parentInvocation, ) : super( @@ -114,8 +113,8 @@ class _FakeTuple2_5 extends _i1.SmartFake ); } -class _FakeHTTP_6 extends _i1.SmartFake implements _i8.HTTP { - _FakeHTTP_6( +class _FakeIsar_6 extends _i1.SmartFake implements _i8.Isar { + _FakeIsar_6( Object parent, Invocation parentInvocation, ) : super( @@ -124,19 +123,9 @@ class _FakeHTTP_6 extends _i1.SmartFake implements _i8.HTTP { ); } -class _FakeIsar_7 extends _i1.SmartFake implements _i9.Isar { - _FakeIsar_7( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeQueryBuilder_8 extends _i1.SmartFake - implements _i9.QueryBuilder { - _FakeQueryBuilder_8( +class _FakeQueryBuilder_7 extends _i1.SmartFake + implements _i8.QueryBuilder { + _FakeQueryBuilder_7( Object parent, Invocation parentInvocation, ) : super( @@ -148,7 +137,7 @@ class _FakeQueryBuilder_8 extends _i1.SmartFake /// A class which mocks [Wallets]. /// /// See the documentation for Mockito's code generation for more information. -class MockWallets extends _i1.Mock implements _i10.Wallets { +class MockWallets extends _i1.Mock implements _i9.Wallets { MockWallets() { _i1.throwOnMissingStub(this); } @@ -221,9 +210,9 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { ); @override - _i11.Future deleteWallet( - _i12.WalletInfo? info, - _i13.SecureStorageInterface? secureStorage, + _i10.Future deleteWallet( + _i11.WalletInfo? info, + _i12.SecureStorageInterface? secureStorage, ) => (super.noSuchMethod( Invocation.method( @@ -233,13 +222,13 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { secureStorage, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future load( - _i14.Prefs? prefs, + _i10.Future load( + _i13.Prefs? prefs, _i3.MainDB? mainDB, ) => (super.noSuchMethod( @@ -250,13 +239,13 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { mainDB, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future loadAfterStackRestore( - _i14.Prefs? prefs, + _i10.Future loadAfterStackRestore( + _i13.Prefs? prefs, List<_i5.Wallet<_i4.CryptoCurrency>>? wallets, bool? isDesktop, ) => @@ -269,15 +258,15 @@ class MockWallets extends _i1.Mock implements _i10.Wallets { isDesktop, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i15.LocaleService { +class MockLocaleService extends _i1.Mock implements _i14.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -285,7 +274,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { @override String get locale => (super.noSuchMethod( Invocation.getter(#locale), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#locale), ), @@ -298,18 +287,18 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { ) as bool); @override - _i11.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( + _i10.Future loadLocale({bool? notify = true}) => (super.noSuchMethod( Invocation.method( #loadLocale, [], {#notify: notify}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -318,7 +307,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -348,7 +337,7 @@ class MockLocaleService extends _i1.Mock implements _i15.LocaleService { /// A class which mocks [Prefs]. /// /// See the documentation for Mockito's code generation for more information. -class MockPrefs extends _i1.Mock implements _i14.Prefs { +class MockPrefs extends _i1.Mock implements _i13.Prefs { MockPrefs() { _i1.throwOnMissingStub(this); } @@ -412,13 +401,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i18.SyncingType get syncType => (super.noSuchMethod( + _i17.SyncingType get syncType => (super.noSuchMethod( Invocation.getter(#syncType), - returnValue: _i18.SyncingType.currentWalletOnly, - ) as _i18.SyncingType); + returnValue: _i17.SyncingType.currentWalletOnly, + ) as _i17.SyncingType); @override - set syncType(_i18.SyncingType? syncType) => super.noSuchMethod( + set syncType(_i17.SyncingType? syncType) => super.noSuchMethod( Invocation.setter( #syncType, syncType, @@ -459,7 +448,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get language => (super.noSuchMethod( Invocation.getter(#language), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#language), ), @@ -477,7 +466,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get currency => (super.noSuchMethod( Invocation.getter(#currency), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#currency), ), @@ -607,13 +596,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i19.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( + _i18.BackupFrequencyType get backupFrequencyType => (super.noSuchMethod( Invocation.getter(#backupFrequencyType), - returnValue: _i19.BackupFrequencyType.everyTenMinutes, - ) as _i19.BackupFrequencyType); + returnValue: _i18.BackupFrequencyType.everyTenMinutes, + ) as _i18.BackupFrequencyType); @override - set backupFrequencyType(_i19.BackupFrequencyType? backupFrequencyType) => + set backupFrequencyType(_i18.BackupFrequencyType? backupFrequencyType) => super.noSuchMethod( Invocation.setter( #backupFrequencyType, @@ -720,7 +709,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get themeId => (super.noSuchMethod( Invocation.getter(#themeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themeId), ), @@ -738,7 +727,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get systemBrightnessLightThemeId => (super.noSuchMethod( Invocation.getter(#systemBrightnessLightThemeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#systemBrightnessLightThemeId), ), @@ -757,7 +746,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { @override String get systemBrightnessDarkThemeId => (super.noSuchMethod( Invocation.getter(#systemBrightnessDarkThemeId), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#systemBrightnessDarkThemeId), ), @@ -843,13 +832,13 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - _i20.Level get logLevel => (super.noSuchMethod( + _i19.Level get logLevel => (super.noSuchMethod( Invocation.getter(#logLevel), - returnValue: _i20.Level.all, - ) as _i20.Level); + returnValue: _i19.Level.all, + ) as _i19.Level); @override - set logLevel(_i20.Level? logLevel) => super.noSuchMethod( + set logLevel(_i19.Level? logLevel) => super.noSuchMethod( Invocation.setter( #logLevel, logLevel, @@ -864,67 +853,67 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ) as bool); @override - _i11.Future init() => (super.noSuchMethod( + _i10.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future isExternalCallsSet() => (super.noSuchMethod( + _i10.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future saveUserID(String? userId) => (super.noSuchMethod( + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i21.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i21.AmountUnit.normal, - ) as _i21.AmountUnit); + returnValue: _i20.AmountUnit.normal, + ) as _i20.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i21.AmountUnit? amountUnit, + required _i20.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -997,7 +986,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1006,7 +995,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1036,7 +1025,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { /// A class which mocks [PriceService]. /// /// See the documentation for Mockito's code generation for more information. -class MockPriceService extends _i1.Mock implements _i22.PriceService { +class MockPriceService extends _i1.Mock implements _i21.PriceService { MockPriceService() { _i1.throwOnMissingStub(this); } @@ -1044,7 +1033,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { @override String get baseTicker => (super.noSuchMethod( Invocation.getter(#baseTicker), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#baseTicker), ), @@ -1069,11 +1058,11 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ) as Duration); @override - _i11.Future> get tokenContractAddressesToCheck => + _i10.Future> get tokenContractAddressesToCheck => (super.noSuchMethod( Invocation.getter(#tokenContractAddressesToCheck), - returnValue: _i11.Future>.value({}), - ) as _i11.Future>); + returnValue: _i10.Future>.value({}), + ) as _i10.Future>); @override bool get hasListeners => (super.noSuchMethod( @@ -1082,46 +1071,30 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ) as bool); @override - _i7.Tuple2<_i23.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => - (super.noSuchMethod( - Invocation.method( - #getPrice, - [coin], - ), - returnValue: _FakeTuple2_5<_i23.Decimal, double>( - this, - Invocation.method( - #getPrice, - [coin], - ), - ), - ) as _i7.Tuple2<_i23.Decimal, double>); + ({double change24h, _i22.Decimal value})? getPrice( + _i4.CryptoCurrency? coin) => + (super.noSuchMethod(Invocation.method( + #getPrice, + [coin], + )) as ({double change24h, _i22.Decimal value})?); @override - _i7.Tuple2<_i23.Decimal, double> getTokenPrice(String? contractAddress) => - (super.noSuchMethod( - Invocation.method( - #getTokenPrice, - [contractAddress], - ), - returnValue: _FakeTuple2_5<_i23.Decimal, double>( - this, - Invocation.method( - #getTokenPrice, - [contractAddress], - ), - ), - ) as _i7.Tuple2<_i23.Decimal, double>); + ({double change24h, _i22.Decimal value})? getTokenPrice( + String? contractAddress) => + (super.noSuchMethod(Invocation.method( + #getTokenPrice, + [contractAddress], + )) as ({double change24h, _i22.Decimal value})?); @override - _i11.Future updatePrice() => (super.noSuchMethod( + _i10.Future updatePrice() => (super.noSuchMethod( Invocation.method( #updatePrice, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override void cancel() => super.noSuchMethod( @@ -1151,7 +1124,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1160,7 +1133,7 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1181,22 +1154,22 @@ class MockPriceService extends _i1.Mock implements _i22.PriceService { /// A class which mocks [ThemeService]. /// /// See the documentation for Mockito's code generation for more information. -class MockThemeService extends _i1.Mock implements _i24.ThemeService { +class MockThemeService extends _i1.Mock implements _i23.ThemeService { MockThemeService() { _i1.throwOnMissingStub(this); } @override - _i8.HTTP get client => (super.noSuchMethod( + _i7.HTTP get client => (super.noSuchMethod( Invocation.getter(#client), - returnValue: _FakeHTTP_6( + returnValue: _FakeHTTP_5( this, Invocation.getter(#client), ), - ) as _i8.HTTP); + ) as _i7.HTTP); @override - set client(_i8.HTTP? _client) => super.noSuchMethod( + set client(_i7.HTTP? _client) => super.noSuchMethod( Invocation.setter( #client, _client, @@ -1214,10 +1187,10 @@ class MockThemeService extends _i1.Mock implements _i24.ThemeService { ) as _i3.MainDB); @override - List<_i25.StackTheme> get installedThemes => (super.noSuchMethod( + List<_i24.StackTheme> get installedThemes => (super.noSuchMethod( Invocation.getter(#installedThemes), - returnValue: <_i25.StackTheme>[], - ) as List<_i25.StackTheme>); + returnValue: <_i24.StackTheme>[], + ) as List<_i24.StackTheme>); @override void init(_i3.MainDB? db) => super.noSuchMethod( @@ -1229,79 +1202,79 @@ class MockThemeService extends _i1.Mock implements _i24.ThemeService { ); @override - _i11.Future install({required _i26.Uint8List? themeArchiveData}) => + _i10.Future install({required _i25.Uint8List? themeArchiveData}) => (super.noSuchMethod( Invocation.method( #install, [], {#themeArchiveData: themeArchiveData}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future remove({required String? themeId}) => (super.noSuchMethod( + _i10.Future remove({required String? themeId}) => (super.noSuchMethod( Invocation.method( #remove, [], {#themeId: themeId}, ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( + _i10.Future checkDefaultThemesOnStartup() => (super.noSuchMethod( Invocation.method( #checkDefaultThemesOnStartup, [], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future verifyInstalled({required String? themeId}) => + _i10.Future verifyInstalled({required String? themeId}) => (super.noSuchMethod( Invocation.method( #verifyInstalled, [], {#themeId: themeId}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future> fetchThemes() => + _i10.Future> fetchThemes() => (super.noSuchMethod( Invocation.method( #fetchThemes, [], ), - returnValue: _i11.Future>.value( - <_i24.StackThemeMetaData>[]), - ) as _i11.Future>); + returnValue: _i10.Future>.value( + <_i23.StackThemeMetaData>[]), + ) as _i10.Future>); @override - _i11.Future<_i26.Uint8List> fetchTheme( - {required _i24.StackThemeMetaData? themeMetaData}) => + _i10.Future<_i25.Uint8List> fetchTheme( + {required _i23.StackThemeMetaData? themeMetaData}) => (super.noSuchMethod( Invocation.method( #fetchTheme, [], {#themeMetaData: themeMetaData}, ), - returnValue: _i11.Future<_i26.Uint8List>.value(_i26.Uint8List(0)), - ) as _i11.Future<_i26.Uint8List>); + returnValue: _i10.Future<_i25.Uint8List>.value(_i25.Uint8List(0)), + ) as _i10.Future<_i25.Uint8List>); @override - _i25.StackTheme? getTheme({required String? themeId}) => + _i24.StackTheme? getTheme({required String? themeId}) => (super.noSuchMethod(Invocation.method( #getTheme, [], {#themeId: themeId}, - )) as _i25.StackTheme?); + )) as _i24.StackTheme?); } /// A class which mocks [MainDB]. @@ -1313,166 +1286,166 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { } @override - _i9.Isar get isar => (super.noSuchMethod( + _i8.Isar get isar => (super.noSuchMethod( Invocation.getter(#isar), - returnValue: _FakeIsar_7( + returnValue: _FakeIsar_6( this, Invocation.getter(#isar), ), - ) as _i9.Isar); + ) as _i8.Isar); @override - _i11.Future initMainDB({_i9.Isar? mock}) => (super.noSuchMethod( + _i10.Future initMainDB({_i8.Isar? mock}) => (super.noSuchMethod( Invocation.method( #initMainDB, [], {#mock: mock}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future putWalletInfo(_i12.WalletInfo? walletInfo) => + _i10.Future putWalletInfo(_i11.WalletInfo? walletInfo) => (super.noSuchMethod( Invocation.method( #putWalletInfo, [walletInfo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future updateWalletInfo(_i12.WalletInfo? walletInfo) => + _i10.Future updateWalletInfo(_i11.WalletInfo? walletInfo) => (super.noSuchMethod( Invocation.method( #updateWalletInfo, [walletInfo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - List<_i27.ContactEntry> getContactEntries() => (super.noSuchMethod( + List<_i26.ContactEntry> getContactEntries() => (super.noSuchMethod( Invocation.method( #getContactEntries, [], ), - returnValue: <_i27.ContactEntry>[], - ) as List<_i27.ContactEntry>); + returnValue: <_i26.ContactEntry>[], + ) as List<_i26.ContactEntry>); @override - _i11.Future deleteContactEntry({required String? id}) => + _i10.Future deleteContactEntry({required String? id}) => (super.noSuchMethod( Invocation.method( #deleteContactEntry, [], {#id: id}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Future isContactEntryExists({required String? id}) => + _i10.Future isContactEntryExists({required String? id}) => (super.noSuchMethod( Invocation.method( #isContactEntryExists, [], {#id: id}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i27.ContactEntry? getContactEntry({required String? id}) => + _i26.ContactEntry? getContactEntry({required String? id}) => (super.noSuchMethod(Invocation.method( #getContactEntry, [], {#id: id}, - )) as _i27.ContactEntry?); + )) as _i26.ContactEntry?); @override - _i11.Future putContactEntry( - {required _i27.ContactEntry? contactEntry}) => + _i10.Future putContactEntry( + {required _i26.ContactEntry? contactEntry}) => (super.noSuchMethod( Invocation.method( #putContactEntry, [], {#contactEntry: contactEntry}, ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i28.TransactionBlockExplorer? getTransactionBlockExplorer( + _i27.TransactionBlockExplorer? getTransactionBlockExplorer( {required _i4.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod(Invocation.method( #getTransactionBlockExplorer, [], {#cryptoCurrency: cryptoCurrency}, - )) as _i28.TransactionBlockExplorer?); + )) as _i27.TransactionBlockExplorer?); @override - _i11.Future putTransactionBlockExplorer( - _i28.TransactionBlockExplorer? explorer) => + _i10.Future putTransactionBlockExplorer( + _i27.TransactionBlockExplorer? explorer) => (super.noSuchMethod( Invocation.method( #putTransactionBlockExplorer, [explorer], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i9.QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.Address, _i28.Address, _i8.QAfterWhereClause> getAddresses(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddresses, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.Address, _i29.Address, - _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.Address, _i28.Address, + _i8.QAfterWhereClause>( this, Invocation.method( #getAddresses, [walletId], ), ), - ) as _i9 - .QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause>); + ) as _i8 + .QueryBuilder<_i28.Address, _i28.Address, _i8.QAfterWhereClause>); @override - _i11.Future putAddress(_i29.Address? address) => (super.noSuchMethod( + _i10.Future putAddress(_i28.Address? address) => (super.noSuchMethod( Invocation.method( #putAddress, [address], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future> putAddresses(List<_i29.Address>? addresses) => + _i10.Future> putAddresses(List<_i28.Address>? addresses) => (super.noSuchMethod( Invocation.method( #putAddresses, [addresses], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future> updateOrPutAddresses(List<_i29.Address>? addresses) => + _i10.Future> updateOrPutAddresses(List<_i28.Address>? addresses) => (super.noSuchMethod( Invocation.method( #updateOrPutAddresses, [addresses], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future<_i29.Address?> getAddress( + _i10.Future<_i28.Address?> getAddress( String? walletId, String? address, ) => @@ -1484,13 +1457,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _i11.Future<_i29.Address?>.value(), - ) as _i11.Future<_i29.Address?>); + returnValue: _i10.Future<_i28.Address?>.value(), + ) as _i10.Future<_i28.Address?>); @override - _i11.Future updateAddress( - _i29.Address? oldAddress, - _i29.Address? newAddress, + _i10.Future updateAddress( + _i28.Address? oldAddress, + _i28.Address? newAddress, ) => (super.noSuchMethod( Invocation.method( @@ -1500,50 +1473,50 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { newAddress, ], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.Transaction, _i28.Transaction, _i8.QAfterWhereClause> getTransactions(String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactions, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.Transaction, _i29.Transaction, - _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.Transaction, _i28.Transaction, + _i8.QAfterWhereClause>( this, Invocation.method( #getTransactions, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.Transaction, _i28.Transaction, + _i8.QAfterWhereClause>); @override - _i11.Future putTransaction(_i29.Transaction? transaction) => + _i10.Future putTransaction(_i28.Transaction? transaction) => (super.noSuchMethod( Invocation.method( #putTransaction, [transaction], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future> putTransactions( - List<_i29.Transaction>? transactions) => + _i10.Future> putTransactions( + List<_i28.Transaction>? transactions) => (super.noSuchMethod( Invocation.method( #putTransactions, [transactions], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i11.Future<_i29.Transaction?> getTransaction( + _i10.Future<_i28.Transaction?> getTransaction( String? walletId, String? txid, ) => @@ -1555,11 +1528,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i29.Transaction?>.value(), - ) as _i11.Future<_i29.Transaction?>); + returnValue: _i10.Future<_i28.Transaction?>.value(), + ) as _i10.Future<_i28.Transaction?>); @override - _i11.Stream<_i29.Transaction?> watchTransaction({ + _i10.Stream<_i28.Transaction?> watchTransaction({ required int? id, bool? fireImmediately = false, }) => @@ -1572,11 +1545,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.Transaction?>.empty(), - ) as _i11.Stream<_i29.Transaction?>); + returnValue: _i10.Stream<_i28.Transaction?>.empty(), + ) as _i10.Stream<_i28.Transaction?>); @override - _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause> getUTXOs( + _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause> getUTXOs( String? walletId) => (super.noSuchMethod( Invocation.method( @@ -1584,17 +1557,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), returnValue: - _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>( + _FakeQueryBuilder_7<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause>( this, Invocation.method( #getUTXOs, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterWhereClause>); @override - _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition> + _i8.QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterFilterCondition> getUTXOsByAddress( String? walletId, String? address, @@ -1607,8 +1580,8 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, - _i9.QAfterFilterCondition>( + returnValue: _FakeQueryBuilder_7<_i28.UTXO, _i28.UTXO, + _i8.QAfterFilterCondition>( this, Invocation.method( #getUTXOsByAddress, @@ -1618,33 +1591,33 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ], ), ), - ) as _i9 - .QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>); + ) as _i8 + .QueryBuilder<_i28.UTXO, _i28.UTXO, _i8.QAfterFilterCondition>); @override - _i11.Future putUTXO(_i29.UTXO? utxo) => (super.noSuchMethod( + _i10.Future putUTXO(_i28.UTXO? utxo) => (super.noSuchMethod( Invocation.method( #putUTXO, [utxo], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future putUTXOs(List<_i29.UTXO>? utxos) => (super.noSuchMethod( + _i10.Future putUTXOs(List<_i28.UTXO>? utxos) => (super.noSuchMethod( Invocation.method( #putUTXOs, [utxos], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future updateUTXOs( + _i10.Future updateUTXOs( String? walletId, - List<_i29.UTXO>? utxos, + List<_i28.UTXO>? utxos, ) => (super.noSuchMethod( Invocation.method( @@ -1654,11 +1627,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { utxos, ], ), - returnValue: _i11.Future.value(false), - ) as _i11.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i11.Stream<_i29.UTXO?> watchUTXO({ + _i10.Stream<_i28.UTXO?> watchUTXO({ required int? id, bool? fireImmediately = false, }) => @@ -1671,54 +1644,54 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.UTXO?>.empty(), - ) as _i11.Stream<_i29.UTXO?>); + returnValue: _i10.Stream<_i28.UTXO?>.empty(), + ) as _i10.Stream<_i28.UTXO?>); @override - _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, - _i9.QAfterWhereClause> getTransactionNotes( + _i8.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i8.QAfterWhereClause> getTransactionNotes( String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactionNotes, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.TransactionNote, - _i29.TransactionNote, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.TransactionNote, + _i28.TransactionNote, _i8.QAfterWhereClause>( this, Invocation.method( #getTransactionNotes, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i8.QAfterWhereClause>); @override - _i11.Future putTransactionNote(_i29.TransactionNote? transactionNote) => + _i10.Future putTransactionNote(_i28.TransactionNote? transactionNote) => (super.noSuchMethod( Invocation.method( #putTransactionNote, [transactionNote], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future putTransactionNotes( - List<_i29.TransactionNote>? transactionNotes) => + _i10.Future putTransactionNotes( + List<_i28.TransactionNote>? transactionNotes) => (super.noSuchMethod( Invocation.method( #putTransactionNotes, [transactionNotes], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future<_i29.TransactionNote?> getTransactionNote( + _i10.Future<_i28.TransactionNote?> getTransactionNote( String? walletId, String? txid, ) => @@ -1730,11 +1703,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i29.TransactionNote?>.value(), - ) as _i11.Future<_i29.TransactionNote?>); + returnValue: _i10.Future<_i28.TransactionNote?>.value(), + ) as _i10.Future<_i28.TransactionNote?>); @override - _i11.Stream<_i29.TransactionNote?> watchTransactionNote({ + _i10.Stream<_i28.TransactionNote?> watchTransactionNote({ required int? id, bool? fireImmediately = false, }) => @@ -1747,39 +1720,39 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.TransactionNote?>.empty(), - ) as _i11.Stream<_i29.TransactionNote?>); + returnValue: _i10.Stream<_i28.TransactionNote?>.empty(), + ) as _i10.Stream<_i28.TransactionNote?>); @override - _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause> + _i8.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, _i8.QAfterWhereClause> getAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddressLabels, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i29.AddressLabel, - _i29.AddressLabel, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_7<_i28.AddressLabel, + _i28.AddressLabel, _i8.QAfterWhereClause>( this, Invocation.method( #getAddressLabels, [walletId], ), ), - ) as _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, - _i9.QAfterWhereClause>); + ) as _i8.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, + _i8.QAfterWhereClause>); @override - _i11.Future putAddressLabel(_i29.AddressLabel? addressLabel) => + _i10.Future putAddressLabel(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabel, [addressLabel], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - int putAddressLabelSync(_i29.AddressLabel? addressLabel) => + int putAddressLabelSync(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabelSync, @@ -1789,18 +1762,18 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as int); @override - _i11.Future putAddressLabels(List<_i29.AddressLabel>? addressLabels) => + _i10.Future putAddressLabels(List<_i28.AddressLabel>? addressLabels) => (super.noSuchMethod( Invocation.method( #putAddressLabels, [addressLabels], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future<_i29.AddressLabel?> getAddressLabel( + _i10.Future<_i28.AddressLabel?> getAddressLabel( String? walletId, String? addressString, ) => @@ -1812,11 +1785,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { addressString, ], ), - returnValue: _i11.Future<_i29.AddressLabel?>.value(), - ) as _i11.Future<_i29.AddressLabel?>); + returnValue: _i10.Future<_i28.AddressLabel?>.value(), + ) as _i10.Future<_i28.AddressLabel?>); @override - _i29.AddressLabel? getAddressLabelSync( + _i28.AddressLabel? getAddressLabelSync( String? walletId, String? addressString, ) => @@ -1826,10 +1799,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, addressString, ], - )) as _i29.AddressLabel?); + )) as _i28.AddressLabel?); @override - _i11.Stream<_i29.AddressLabel?> watchAddressLabel({ + _i10.Stream<_i28.AddressLabel?> watchAddressLabel({ required int? id, bool? fireImmediately = false, }) => @@ -1842,55 +1815,55 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i29.AddressLabel?>.empty(), - ) as _i11.Stream<_i29.AddressLabel?>); + returnValue: _i10.Stream<_i28.AddressLabel?>.empty(), + ) as _i10.Stream<_i28.AddressLabel?>); @override - _i11.Future updateAddressLabel(_i29.AddressLabel? addressLabel) => + _i10.Future updateAddressLabel(_i28.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #updateAddressLabel, [addressLabel], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future deleteWalletBlockchainData(String? walletId) => + _i10.Future deleteWalletBlockchainData(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteWalletBlockchainData, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future deleteAddressLabels(String? walletId) => + _i10.Future deleteAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteAddressLabels, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future deleteTransactionNotes(String? walletId) => + _i10.Future deleteTransactionNotes(String? walletId) => (super.noSuchMethod( Invocation.method( #deleteTransactionNotes, [walletId], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future addNewTransactionData( - List<_i7.Tuple2<_i29.Transaction, _i29.Address?>>? transactionsData, + _i10.Future addNewTransactionData( + List<_i29.Tuple2<_i28.Transaction, _i28.Address?>>? transactionsData, String? walletId, ) => (super.noSuchMethod( @@ -1901,93 +1874,93 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, ], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future> updateOrPutTransactionV2s( + _i10.Future> updateOrPutTransactionV2s( List<_i30.TransactionV2>? transactions) => (super.noSuchMethod( Invocation.method( #updateOrPutTransactionV2s, [transactions], ), - returnValue: _i11.Future>.value([]), - ) as _i11.Future>); + returnValue: _i10.Future>.value([]), + ) as _i10.Future>); @override - _i9.QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere> + _i8.QueryBuilder<_i28.EthContract, _i28.EthContract, _i8.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method( #getEthContracts, [], ), - returnValue: _FakeQueryBuilder_8<_i29.EthContract, _i29.EthContract, - _i9.QWhere>( + returnValue: _FakeQueryBuilder_7<_i28.EthContract, _i28.EthContract, + _i8.QWhere>( this, Invocation.method( #getEthContracts, [], ), ), - ) as _i9 - .QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere>); + ) as _i8 + .QueryBuilder<_i28.EthContract, _i28.EthContract, _i8.QWhere>); @override - _i11.Future<_i29.EthContract?> getEthContract(String? contractAddress) => + _i10.Future<_i28.EthContract?> getEthContract(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getEthContract, [contractAddress], ), - returnValue: _i11.Future<_i29.EthContract?>.value(), - ) as _i11.Future<_i29.EthContract?>); + returnValue: _i10.Future<_i28.EthContract?>.value(), + ) as _i10.Future<_i28.EthContract?>); @override - _i29.EthContract? getEthContractSync(String? contractAddress) => + _i28.EthContract? getEthContractSync(String? contractAddress) => (super.noSuchMethod(Invocation.method( #getEthContractSync, [contractAddress], - )) as _i29.EthContract?); + )) as _i28.EthContract?); @override - _i11.Future putEthContract(_i29.EthContract? contract) => + _i10.Future putEthContract(_i28.EthContract? contract) => (super.noSuchMethod( Invocation.method( #putEthContract, [contract], ), - returnValue: _i11.Future.value(0), - ) as _i11.Future); + returnValue: _i10.Future.value(0), + ) as _i10.Future); @override - _i11.Future putEthContracts(List<_i29.EthContract>? contracts) => + _i10.Future putEthContracts(List<_i28.EthContract>? contracts) => (super.noSuchMethod( Invocation.method( #putEthContracts, [contracts], ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i11.Future getHighestUsedMintIndex({required String? walletId}) => + _i10.Future getHighestUsedMintIndex({required String? walletId}) => (super.noSuchMethod( Invocation.method( #getHighestUsedMintIndex, [], {#walletId: walletId}, ), - returnValue: _i11.Future.value(), - ) as _i11.Future); + returnValue: _i10.Future.value(), + ) as _i10.Future); } /// A class which mocks [IThemeAssets]. /// /// See the documentation for Mockito's code generation for more information. -class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { +class MockIThemeAssets extends _i1.Mock implements _i24.IThemeAssets { MockIThemeAssets() { _i1.throwOnMissingStub(this); } @@ -1995,7 +1968,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get bellNew => (super.noSuchMethod( Invocation.getter(#bellNew), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#bellNew), ), @@ -2004,7 +1977,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get buy => (super.noSuchMethod( Invocation.getter(#buy), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#buy), ), @@ -2013,7 +1986,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get exchange => (super.noSuchMethod( Invocation.getter(#exchange), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#exchange), ), @@ -2022,7 +1995,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get personaIncognito => (super.noSuchMethod( Invocation.getter(#personaIncognito), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#personaIncognito), ), @@ -2031,7 +2004,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get personaEasy => (super.noSuchMethod( Invocation.getter(#personaEasy), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#personaEasy), ), @@ -2040,7 +2013,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get stack => (super.noSuchMethod( Invocation.getter(#stack), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#stack), ), @@ -2049,7 +2022,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get stackIcon => (super.noSuchMethod( Invocation.getter(#stackIcon), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#stackIcon), ), @@ -2058,7 +2031,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receive => (super.noSuchMethod( Invocation.getter(#receive), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receive), ), @@ -2067,7 +2040,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receivePending => (super.noSuchMethod( Invocation.getter(#receivePending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receivePending), ), @@ -2076,7 +2049,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get receiveCancelled => (super.noSuchMethod( Invocation.getter(#receiveCancelled), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#receiveCancelled), ), @@ -2085,7 +2058,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get send => (super.noSuchMethod( Invocation.getter(#send), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#send), ), @@ -2094,7 +2067,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get sendPending => (super.noSuchMethod( Invocation.getter(#sendPending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#sendPending), ), @@ -2103,7 +2076,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get sendCancelled => (super.noSuchMethod( Invocation.getter(#sendCancelled), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#sendCancelled), ), @@ -2112,7 +2085,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get themeSelector => (super.noSuchMethod( Invocation.getter(#themeSelector), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themeSelector), ), @@ -2121,7 +2094,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get themePreview => (super.noSuchMethod( Invocation.getter(#themePreview), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#themePreview), ), @@ -2130,7 +2103,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchange => (super.noSuchMethod( Invocation.getter(#txExchange), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchange), ), @@ -2139,7 +2112,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchangePending => (super.noSuchMethod( Invocation.getter(#txExchangePending), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchangePending), ), @@ -2148,7 +2121,7 @@ class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { @override String get txExchangeFailed => (super.noSuchMethod( Invocation.getter(#txExchangeFailed), - returnValue: _i16.dummyValue( + returnValue: _i15.dummyValue( this, Invocation.getter(#txExchangeFailed), ),