diff --git a/docs/building.md b/docs/building.md index 3ee68cf98..6aa647e53 100644 --- a/docs/building.md +++ b/docs/building.md @@ -7,6 +7,7 @@ Here you will find instructions on how to install the necessary tools for buildi - The only OS supported for building Android and Linux desktop is Ubuntu 20.04. Windows builds require using Ubuntu 20.04 on WSL2. macOS builds for itself and iOS. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) - 100 GB of storage +- Install go: [https://go.dev/doc/install](https://go.dev/doc/install) ## Linux host @@ -163,6 +164,8 @@ cd scripts/windows ./deps.sh ``` +install go in WSL [https://go.dev/doc/install](https://go.dev/doc/install) (follow linux instructions) and ensure you have `x86_64-w64-mingw32-gcc` + and use `scripts/build_app.sh` to build plugins: ``` cd .. diff --git a/lib/db/drift/database.dart b/lib/db/drift/database.dart index 36cb67150..4bfd94341 100644 --- a/lib/db/drift/database.dart +++ b/lib/db/drift/database.dart @@ -9,6 +9,7 @@ */ import 'dart:async'; +import 'dart:math' as math; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; @@ -44,13 +45,56 @@ class SparkNames extends Table { Set get primaryKey => {name}; } -@DriftDatabase(tables: [SparkNames]) +class MwebUtxos extends Table { + TextColumn get outputId => text()(); + TextColumn get address => text()(); + IntColumn get value => integer()(); + IntColumn get height => integer()(); + IntColumn get blockTime => integer()(); + BoolColumn get blocked => boolean()(); + BoolColumn get used => boolean()(); + + @override + Set get primaryKey => {outputId}; +} + +extension MwebUtxoExt on MwebUtxo { + int getConfirmations(int currentChainHeight) { + if (blockTime <= 0) return 0; + if (height <= 0) return 0; + return math.max(0, currentChainHeight - (height - 1)); + } + + bool isConfirmed( + int currentChainHeight, + int minimumConfirms, { + int? overrideMinConfirms, + }) { + final confirmations = getConfirmations(currentChainHeight); + + if (overrideMinConfirms != null) { + return confirmations >= overrideMinConfirms; + } + return confirmations >= minimumConfirms; + } +} + +@DriftDatabase(tables: [SparkNames, MwebUtxos]) final class WalletDatabase extends _$WalletDatabase { WalletDatabase._(String walletId, [QueryExecutor? executor]) : super(executor ?? _openConnection(walletId)); @override - int get schemaVersion => 1; + int get schemaVersion => 2; + + @override + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + if (from == 1 && to == 2) { + await m.createTable(mwebUtxos); + } + }, + ); static QueryExecutor _openConnection(String walletId) { return driftDatabase( diff --git a/lib/db/drift/database.g.dart b/lib/db/drift/database.g.dart index 42113b071..67660d3a6 100644 --- a/lib/db/drift/database.g.dart +++ b/lib/db/drift/database.g.dart @@ -287,15 +287,405 @@ class SparkNamesCompanion extends UpdateCompanion { } } +class $MwebUtxosTable extends MwebUtxos + with TableInfo<$MwebUtxosTable, MwebUtxo> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MwebUtxosTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _outputIdMeta = + const VerificationMeta('outputId'); + @override + late final GeneratedColumn outputId = GeneratedColumn( + 'output_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _addressMeta = + const VerificationMeta('address'); + @override + late final GeneratedColumn address = GeneratedColumn( + 'address', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _valueMeta = const VerificationMeta('value'); + @override + late final GeneratedColumn value = GeneratedColumn( + 'value', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _heightMeta = const VerificationMeta('height'); + @override + late final GeneratedColumn height = GeneratedColumn( + 'height', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockTimeMeta = + const VerificationMeta('blockTime'); + @override + late final GeneratedColumn blockTime = GeneratedColumn( + 'block_time', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: true); + static const VerificationMeta _blockedMeta = + const VerificationMeta('blocked'); + @override + late final GeneratedColumn blocked = GeneratedColumn( + 'blocked', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("blocked" IN (0, 1))')); + static const VerificationMeta _usedMeta = const VerificationMeta('used'); + @override + late final GeneratedColumn used = GeneratedColumn( + 'used', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("used" IN (0, 1))')); + @override + List get $columns => + [outputId, address, value, height, blockTime, blocked, used]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'mweb_utxos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('output_id')) { + context.handle(_outputIdMeta, + outputId.isAcceptableOrUnknown(data['output_id']!, _outputIdMeta)); + } else if (isInserting) { + context.missing(_outputIdMeta); + } + if (data.containsKey('address')) { + context.handle(_addressMeta, + address.isAcceptableOrUnknown(data['address']!, _addressMeta)); + } else if (isInserting) { + context.missing(_addressMeta); + } + if (data.containsKey('value')) { + context.handle( + _valueMeta, value.isAcceptableOrUnknown(data['value']!, _valueMeta)); + } else if (isInserting) { + context.missing(_valueMeta); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } else if (isInserting) { + context.missing(_heightMeta); + } + if (data.containsKey('block_time')) { + context.handle(_blockTimeMeta, + blockTime.isAcceptableOrUnknown(data['block_time']!, _blockTimeMeta)); + } else if (isInserting) { + context.missing(_blockTimeMeta); + } + if (data.containsKey('blocked')) { + context.handle(_blockedMeta, + blocked.isAcceptableOrUnknown(data['blocked']!, _blockedMeta)); + } else if (isInserting) { + context.missing(_blockedMeta); + } + if (data.containsKey('used')) { + context.handle( + _usedMeta, used.isAcceptableOrUnknown(data['used']!, _usedMeta)); + } else if (isInserting) { + context.missing(_usedMeta); + } + return context; + } + + @override + Set get $primaryKey => {outputId}; + @override + MwebUtxo map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MwebUtxo( + outputId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}output_id'])!, + address: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}address'])!, + value: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}value'])!, + height: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}height'])!, + blockTime: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}block_time'])!, + blocked: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}blocked'])!, + used: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}used'])!, + ); + } + + @override + $MwebUtxosTable createAlias(String alias) { + return $MwebUtxosTable(attachedDatabase, alias); + } +} + +class MwebUtxo extends DataClass implements Insertable { + final String outputId; + final String address; + final int value; + final int height; + final int blockTime; + final bool blocked; + final bool used; + const MwebUtxo( + {required this.outputId, + required this.address, + required this.value, + required this.height, + required this.blockTime, + required this.blocked, + required this.used}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['output_id'] = Variable(outputId); + map['address'] = Variable(address); + map['value'] = Variable(value); + map['height'] = Variable(height); + map['block_time'] = Variable(blockTime); + map['blocked'] = Variable(blocked); + map['used'] = Variable(used); + return map; + } + + MwebUtxosCompanion toCompanion(bool nullToAbsent) { + return MwebUtxosCompanion( + outputId: Value(outputId), + address: Value(address), + value: Value(value), + height: Value(height), + blockTime: Value(blockTime), + blocked: Value(blocked), + used: Value(used), + ); + } + + factory MwebUtxo.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MwebUtxo( + outputId: serializer.fromJson(json['outputId']), + address: serializer.fromJson(json['address']), + value: serializer.fromJson(json['value']), + height: serializer.fromJson(json['height']), + blockTime: serializer.fromJson(json['blockTime']), + blocked: serializer.fromJson(json['blocked']), + used: serializer.fromJson(json['used']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'outputId': serializer.toJson(outputId), + 'address': serializer.toJson(address), + 'value': serializer.toJson(value), + 'height': serializer.toJson(height), + 'blockTime': serializer.toJson(blockTime), + 'blocked': serializer.toJson(blocked), + 'used': serializer.toJson(used), + }; + } + + MwebUtxo copyWith( + {String? outputId, + String? address, + int? value, + int? height, + int? blockTime, + bool? blocked, + bool? used}) => + MwebUtxo( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + ); + MwebUtxo copyWithCompanion(MwebUtxosCompanion data) { + return MwebUtxo( + outputId: data.outputId.present ? data.outputId.value : this.outputId, + address: data.address.present ? data.address.value : this.address, + value: data.value.present ? data.value.value : this.value, + height: data.height.present ? data.height.value : this.height, + blockTime: data.blockTime.present ? data.blockTime.value : this.blockTime, + blocked: data.blocked.present ? data.blocked.value : this.blocked, + used: data.used.present ? data.used.value : this.used, + ); + } + + @override + String toString() { + return (StringBuffer('MwebUtxo(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(outputId, address, value, height, blockTime, blocked, used); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MwebUtxo && + other.outputId == this.outputId && + other.address == this.address && + other.value == this.value && + other.height == this.height && + other.blockTime == this.blockTime && + other.blocked == this.blocked && + other.used == this.used); +} + +class MwebUtxosCompanion extends UpdateCompanion { + final Value outputId; + final Value address; + final Value value; + final Value height; + final Value blockTime; + final Value blocked; + final Value used; + final Value rowid; + const MwebUtxosCompanion({ + this.outputId = const Value.absent(), + this.address = const Value.absent(), + this.value = const Value.absent(), + this.height = const Value.absent(), + this.blockTime = const Value.absent(), + this.blocked = const Value.absent(), + this.used = const Value.absent(), + this.rowid = const Value.absent(), + }); + MwebUtxosCompanion.insert({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + this.rowid = const Value.absent(), + }) : outputId = Value(outputId), + address = Value(address), + value = Value(value), + height = Value(height), + blockTime = Value(blockTime), + blocked = Value(blocked), + used = Value(used); + static Insertable custom({ + Expression? outputId, + Expression? address, + Expression? value, + Expression? height, + Expression? blockTime, + Expression? blocked, + Expression? used, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (outputId != null) 'output_id': outputId, + if (address != null) 'address': address, + if (value != null) 'value': value, + if (height != null) 'height': height, + if (blockTime != null) 'block_time': blockTime, + if (blocked != null) 'blocked': blocked, + if (used != null) 'used': used, + if (rowid != null) 'rowid': rowid, + }); + } + + MwebUtxosCompanion copyWith( + {Value? outputId, + Value? address, + Value? value, + Value? height, + Value? blockTime, + Value? blocked, + Value? used, + Value? rowid}) { + return MwebUtxosCompanion( + outputId: outputId ?? this.outputId, + address: address ?? this.address, + value: value ?? this.value, + height: height ?? this.height, + blockTime: blockTime ?? this.blockTime, + blocked: blocked ?? this.blocked, + used: used ?? this.used, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (outputId.present) { + map['output_id'] = Variable(outputId.value); + } + if (address.present) { + map['address'] = Variable(address.value); + } + if (value.present) { + map['value'] = Variable(value.value); + } + if (height.present) { + map['height'] = Variable(height.value); + } + if (blockTime.present) { + map['block_time'] = Variable(blockTime.value); + } + if (blocked.present) { + map['blocked'] = Variable(blocked.value); + } + if (used.present) { + map['used'] = Variable(used.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MwebUtxosCompanion(') + ..write('outputId: $outputId, ') + ..write('address: $address, ') + ..write('value: $value, ') + ..write('height: $height, ') + ..write('blockTime: $blockTime, ') + ..write('blocked: $blocked, ') + ..write('used: $used, ') + ..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); + late final $MwebUtxosTable mwebUtxos = $MwebUtxosTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => [sparkNames]; + List get allSchemaEntities => [sparkNames, mwebUtxos]; } typedef $$SparkNamesTableCreateCompanionBuilder = SparkNamesCompanion Function({ @@ -450,10 +840,207 @@ typedef $$SparkNamesTableProcessedTableManager = ProcessedTableManager< (SparkName, BaseReferences<_$WalletDatabase, $SparkNamesTable, SparkName>), SparkName, PrefetchHooks Function()>; +typedef $$MwebUtxosTableCreateCompanionBuilder = MwebUtxosCompanion Function({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid, +}); +typedef $$MwebUtxosTableUpdateCompanionBuilder = MwebUtxosCompanion Function({ + Value outputId, + Value address, + Value value, + Value height, + Value blockTime, + Value blocked, + Value used, + Value rowid, +}); + +class $$MwebUtxosTableFilterComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get outputId => $composableBuilder( + column: $table.outputId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnFilters(column)); + + ColumnFilters get value => $composableBuilder( + column: $table.value, builder: (column) => ColumnFilters(column)); + + ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blockTime => $composableBuilder( + column: $table.blockTime, builder: (column) => ColumnFilters(column)); + + ColumnFilters get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnFilters(column)); + + ColumnFilters get used => $composableBuilder( + column: $table.used, builder: (column) => ColumnFilters(column)); +} + +class $$MwebUtxosTableOrderingComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get outputId => $composableBuilder( + column: $table.outputId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get address => $composableBuilder( + column: $table.address, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get value => $composableBuilder( + column: $table.value, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blockTime => $composableBuilder( + column: $table.blockTime, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get blocked => $composableBuilder( + column: $table.blocked, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get used => $composableBuilder( + column: $table.used, builder: (column) => ColumnOrderings(column)); +} + +class $$MwebUtxosTableAnnotationComposer + extends Composer<_$WalletDatabase, $MwebUtxosTable> { + $$MwebUtxosTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get outputId => + $composableBuilder(column: $table.outputId, builder: (column) => column); + + GeneratedColumn get address => + $composableBuilder(column: $table.address, builder: (column) => column); + + GeneratedColumn get value => + $composableBuilder(column: $table.value, builder: (column) => column); + + GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + GeneratedColumn get blockTime => + $composableBuilder(column: $table.blockTime, builder: (column) => column); + + GeneratedColumn get blocked => + $composableBuilder(column: $table.blocked, builder: (column) => column); + + GeneratedColumn get used => + $composableBuilder(column: $table.used, builder: (column) => column); +} + +class $$MwebUtxosTableTableManager extends RootTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()> { + $$MwebUtxosTableTableManager(_$WalletDatabase db, $MwebUtxosTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MwebUtxosTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MwebUtxosTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MwebUtxosTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value outputId = const Value.absent(), + Value address = const Value.absent(), + Value value = const Value.absent(), + Value height = const Value.absent(), + Value blockTime = const Value.absent(), + Value blocked = const Value.absent(), + Value used = const Value.absent(), + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + createCompanionCallback: ({ + required String outputId, + required String address, + required int value, + required int height, + required int blockTime, + required bool blocked, + required bool used, + Value rowid = const Value.absent(), + }) => + MwebUtxosCompanion.insert( + outputId: outputId, + address: address, + value: value, + height: height, + blockTime: blockTime, + blocked: blocked, + used: used, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$MwebUtxosTableProcessedTableManager = ProcessedTableManager< + _$WalletDatabase, + $MwebUtxosTable, + MwebUtxo, + $$MwebUtxosTableFilterComposer, + $$MwebUtxosTableOrderingComposer, + $$MwebUtxosTableAnnotationComposer, + $$MwebUtxosTableCreateCompanionBuilder, + $$MwebUtxosTableUpdateCompanionBuilder, + (MwebUtxo, BaseReferences<_$WalletDatabase, $MwebUtxosTable, MwebUtxo>), + MwebUtxo, + PrefetchHooks Function()>; class $WalletDatabaseManager { final _$WalletDatabase _db; $WalletDatabaseManager(this._db); $$SparkNamesTableTableManager get sparkNames => $$SparkNamesTableTableManager(_db, _db.sparkNames); + $$MwebUtxosTableTableManager get mwebUtxos => + $$MwebUtxosTableTableManager(_db, _db.mwebUtxos); } diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index cb0163503..0a0269025 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -312,6 +312,7 @@ class MainDB { Future updateUTXOs(String walletId, List utxos) async { bool newUTXO = false; + await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { @@ -340,7 +341,9 @@ class MainDB { } await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); - await isar.utxos.putAll(set.toList()); + if (set.isNotEmpty) { + await isar.utxos.putAll(set.toList()); + } }); return newUTXO; diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index 3852d9be9..da5cf2778 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -813,7 +813,10 @@ class ElectrumXClient { }) async { Logging.instance.d("attempting to fetch blockchain.transaction.get..."); await checkElectrumAdapter(); - final dynamic response = await getElectrumAdapter()!.getTransaction(txHash); + final dynamic response = await getElectrumAdapter()!.request( + 'blockchain.transaction.get', + [txHash, verbose], + ); Logging.instance.d("Fetching blockchain.transaction.get finished"); if (!verbose) { diff --git a/lib/main.dart b/lib/main.dart index c521236fb..3ffda4012 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,6 +59,7 @@ import 'providers/providers.dart'; import 'route_generator.dart'; import 'services/exchange/exchange_data_loading_service.dart'; import 'services/locale_service.dart'; +import 'services/mwebd_service.dart'; import 'services/node_service.dart'; import 'services/notifications_api.dart'; import 'services/notifications_service.dart'; @@ -73,6 +74,7 @@ import 'utilities/logger.dart'; import 'utilities/prefs.dart'; import 'utilities/stack_file_system.dart'; import 'utilities/util.dart'; +import 'wallets/crypto_currency/crypto_currency.dart'; import 'wallets/isar/providers/all_wallets_info_provider.dart'; import 'wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import 'widgets/crypto_notifications.dart'; @@ -214,6 +216,25 @@ void main(List args) async { await CampfireMigration.init(); } + if (kDebugMode) { + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.main) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: MAINNET]: $line")), + ), + ); + unawaited( + MwebdService.instance + .logsStream(CryptoCurrencyNetwork.test) + .then( + (stream) => + stream.listen((line) => print("[MWEBD: TESTNET]: $line")), + ), + ); + } + // TODO: // This should be moved to happen during the loading animation instead of // showing a blank screen for 4-10 seconds. diff --git a/lib/models/input.dart b/lib/models/input.dart new file mode 100644 index 000000000..2fcfde38d --- /dev/null +++ b/lib/models/input.dart @@ -0,0 +1,113 @@ +/* + * 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:coinlib_flutter/coinlib_flutter.dart'; + +import '../db/drift/database.dart'; +import '../utilities/enums/derive_path_type_enum.dart'; +import 'isar/models/isar_models.dart'; + +abstract class BaseInput { + BaseInput(this._utxo, {this.key}); + + final Object _utxo; + HDKey? key; + + String get id; + + String? get address; + + BigInt get value; + + int? get blockTime; + + @override + String toString() { + return "BaseInput{\n" + " _utxo: $_utxo,\n" + " key: $key,\n" + "}"; + } +} + +class StandardInput extends BaseInput { + StandardInput(UTXO super.utxo, {this.derivePathType, super.key}); + + final DerivePathType? derivePathType; + + UTXO get utxo => _utxo as UTXO; + + @override + String get id => utxo.txid; + + @override + String? get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime; + + @override + String toString() { + return "StandardInput{\n" + " derivePathType: $derivePathType,\n" + " utxo: $utxo,\n" + " key: $key,\n" + "}"; + } + + @override + bool operator ==(Object other) { + return other is StandardInput && + other.utxo.walletId == utxo.walletId && + other.utxo.txid == utxo.txid && + other.utxo.vout == utxo.vout && + other.derivePathType == derivePathType; + } + + @override + int get hashCode => Object.hashAll([utxo.walletId, utxo.txid, utxo.vout]); +} + +class MwebInput extends BaseInput { + MwebInput(MwebUtxo super.utxo); + + MwebUtxo get utxo => _utxo as MwebUtxo; + + @override + String get id => utxo.outputId; + + @override + String get address => utxo.address; + + @override + BigInt get value => BigInt.from(utxo.value); + + @override + int? get blockTime => utxo.blockTime < 1 ? null : utxo.blockTime; + + @override + String toString() { + return "MwebInput{\n" + " utxo: $utxo,\n" + " key: $key,\n" + "}"; + } + + @override + bool operator ==(Object other) { + return other is MwebInput && other.utxo == utxo; + } + + @override + int get hashCode => Object.hashAll([utxo.hashCode]); +} diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index cb239bc5e..c1dfb3a24 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -175,7 +175,8 @@ enum AddressType { solana, cardanoShelley, xelis, - fact0rn; + fact0rn, + mweb; String get readableName { switch (this) { @@ -217,6 +218,8 @@ enum AddressType { return "Xelis"; case AddressType.fact0rn: return "FACT0RN"; + case AddressType.mweb: + return "MWEB"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index a9289a481..e3df46a6b 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -280,6 +280,8 @@ const _AddresstypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -300,6 +302,8 @@ const _AddresstypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _addressGetId(Address object) { diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index d5b3572fe..417856079 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -151,7 +151,8 @@ class Transaction { } @override - toString() => "{ " + toString() => + "{ " "id: $id, " "walletId: $walletId, " "txid: $txid, " @@ -217,12 +218,14 @@ class Transaction { slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, nonce: json["nonce"] as int?, - inputs: List.from(json["inputs"] as List) - .map((e) => Input.fromJsonString(e)) - .toList(), - outputs: List.from(json["outputs"] as List) - .map((e) => Output.fromJsonString(e)) - .toList(), + inputs: + List.from( + json["inputs"] as List, + ).map((e) => Input.fromJsonString(e)).toList(), + outputs: + List.from( + json["outputs"] as List, + ).map((e) => Output.fromJsonString(e)).toList(), numberOfMessages: json["numberOfMessages"] as int, ); if (json["address"] == null) { @@ -241,7 +244,7 @@ enum TransactionType { outgoing, incoming, sentToSelf, // should we keep this? - unknown; + unknown, } // Used in Isar db and stored there as int indexes so adding/removing values @@ -256,5 +259,6 @@ enum TransactionSubType { cashFusion, sparkMint, // firo specific sparkSpend, // firo specific - ordinal; + ordinal, + mweb, } diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 0d34d133d..3d9a3d409 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -368,6 +368,7 @@ const _TransactionsubTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, @@ -379,6 +380,7 @@ const _TransactionsubTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart index 68d8a18c5..6c2ffde6d 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.g.dart @@ -389,6 +389,7 @@ const _TransactionV2subTypeEnumValueMap = { 'sparkMint': 6, 'sparkSpend': 7, 'ordinal': 8, + 'mweb': 9, }; const _TransactionV2subTypeValueEnumMap = { 0: TransactionSubType.none, @@ -400,6 +401,7 @@ const _TransactionV2subTypeValueEnumMap = { 6: TransactionSubType.sparkMint, 7: TransactionSubType.sparkSpend, 8: TransactionSubType.ordinal, + 9: TransactionSubType.mweb, }; const _TransactionV2typeEnumValueMap = { 'outgoing': 0, diff --git a/lib/models/signing_data.dart b/lib/models/signing_data.dart deleted file mode 100644 index 265a021db..000000000 --- a/lib/models/signing_data.dart +++ /dev/null @@ -1,34 +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:coinlib_flutter/coinlib_flutter.dart'; -import 'isar/models/isar_models.dart'; -import '../utilities/enums/derive_path_type_enum.dart'; - -class SigningData { - SigningData({ - required this.derivePathType, - required this.utxo, - this.keyPair, - }); - - final DerivePathType derivePathType; - final UTXO utxo; - HDPrivateKey? keyPair; - - @override - String toString() { - return "SigningData{\n" - " derivePathType: $derivePathType,\n" - " utxo: $utxo,\n" - " keyPair: $keyPair,\n" - "}"; - } -} diff --git a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart index 9b388a5a3..facaeed44 100644 --- a/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart +++ b/lib/pages/add_wallet_views/frost_ms/restore/restore_frost_ms_wallet_view.dart @@ -212,7 +212,7 @@ class _RestoreFrostMsWalletViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); configFieldController.text = qrResult.rawContent; diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 535fbe07a..aeb9ff845 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -596,7 +596,7 @@ class _RestoreWalletViewState extends ConsumerState { Future scanMnemonicQr() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final results = AddressUtils.decodeQRSeedData(qrResult.rawContent); diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index 818393aec..b6dcd7bde 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -70,7 +70,7 @@ class _NewContactAddressEntryFormState // .read(shouldShowLockscreenOnResumeStateProvider // .state) // .state = false; - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index d62a2bdc4..c194ec9ee 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -693,7 +693,7 @@ class _BuyFormState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 621b981ec..a731bf9df 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -69,7 +69,7 @@ class _Step2ViewState extends ConsumerState { void _onRefundQrTapped() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, @@ -122,7 +122,7 @@ class _Step2ViewState extends ConsumerState { void _onToQrTapped() async { try { - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final paymentData = AddressUtils.parsePaymentUri( qrResult.rawContent, 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 72d2ff241..d244ded98 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 @@ -230,10 +230,17 @@ class _Step4ViewState extends ConsumerState { Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + if (wallet is FiroWallet && !firoPublicSend) { txDataFuture = wallet.prepareSendSpark( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [recipient], note: "${model.trade!.payInCurrency.toUpperCase()}/" "${model.trade!.payOutCurrency.toUpperCase()} exchange", @@ -248,7 +255,7 @@ class _Step4ViewState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, note: diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 8f397e79e..fa0283ff4 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -283,6 +283,13 @@ class _SendFromCardState extends ConsumerState { TxData txData; Future txDataFuture; + final recipient = TxRecipient( + address: address, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(address)!, + ); + // if not firo then do normal send if (shouldSendPublicFiroFunds == null) { final memo = @@ -293,7 +300,7 @@ class _SendFromCardState extends ConsumerState { : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [recipient], memo: memo, feeRateType: FeeRateType.average, ), @@ -304,14 +311,14 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [recipient], feeRateType: FeeRateType.average, ), ); } else { txDataFuture = firoWallet.prepareSendSpark( txData: TxData( - recipients: [(address: address, amount: amount, isChange: false)], + recipients: [recipient], // feeRateType: FeeRateType.average, ), ); diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index d0dddda3e..2943c6ec1 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -103,13 +103,14 @@ class _BuyDomainWidgetState extends ConsumerState { note: "Reserve ${widget.domainName.substring(2)}.bit", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: myAddress.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: myAddress.type, ), ], ); @@ -123,28 +124,30 @@ class _BuyDomainWidgetState extends ConsumerState { if (_preRegLock) return; _preRegLock = true; try { - final txData = (await showLoading( - whileFuture: _preRegFuture(), - context: context, - message: "Preparing transaction...", - onException: (e) { - throw e; - }, - ))!; + 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: ConfirmNameTransactionView( - txData: txData, - walletId: widget.walletId, + builder: + (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), ), - ), - ), ); } else { await Navigator.of(context).pushNamed( @@ -164,12 +167,13 @@ class _BuyDomainWidgetState extends ConsumerState { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Error", - message: err, - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -192,50 +196,48 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (context) { return Util.isDesktop ? SDialog( - child: SizedBox( - width: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Add DNS record", - style: STextStyles.desktopH3(context), - ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of( - context, - rootNavigator: true, - ).pop(); - }, + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3(context), ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, ), - child: AddDnsStep1( - name: _getNameFormattedForInternal(), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep1( + name: _getNameFormattedForInternal(), ), - ], - ), + ), + ], ), - ) + ), + ) : StackDialogBase( - child: AddDnsStep1( - name: _getNameFormattedForInternal(), - ), - ); + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), + ); }, ); }, @@ -254,11 +256,12 @@ class _BuyDomainWidgetState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Add DNS record failed", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), + builder: + (_) => StackOkDialog( + title: "Add DNS record failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), ); } } finally { @@ -289,8 +292,9 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (ctx, constraints) { return SingleChildScrollView( child: ConstrainedBox( - constraints: - BoxConstraints(minHeight: constraints.maxHeight), + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), child: IntrinsicHeight( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -306,114 +310,120 @@ class _BuyDomainWidgetState extends ConsumerState { ); }, child: Column( - crossAxisAlignment: Util.isDesktop - ? CrossAxisAlignment.start - : CrossAxisAlignment.stretch, + crossAxisAlignment: + Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ if (!Util.isDesktop) Text( "Buy domain", - style: Util.isDesktop - ? STextStyles.desktopH3(context) - : STextStyles.pageTitleH2(context), + style: + Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), Row( - mainAxisAlignment: Util.isDesktop - ? MainAxisAlignment.center - : MainAxisAlignment.start, + mainAxisAlignment: + Util.isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ Text( "Name registration will take approximately 2 to 4 hours.", - style: Util.isDesktop - ? STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ) - : STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), + style: + Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ) + : STextStyles.w500_12(context).copyWith( + color: + Theme.of( + context, + ).extension()!.textDark3, + ), ), ], ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Domain 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, - ), + 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.domainName.substring(2)}.bit", - style: Util.isDesktop - ? STextStyles.w500_14(context) - : STextStyles.w500_12(context), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), RoundedWhiteContainer( child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Amount", - 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, - ), + 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( + ref + .watch(pAmountFormatter(coin)) + .format( Amount( rawValue: BigInt.from(kNameNewAmountSats), fractionDigits: coin.fractionDigits, ), ), - style: Util.isDesktop - ? STextStyles.w500_14(context) - : STextStyles.w500_12(context), + style: + Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), ConditionalParent( condition: !Util.isDesktop, - builder: (child) => Row( - children: [child], - ), + builder: (child) => Row(children: [child]), child: CustomTextButton( text: _settingsHidden ? "More settings" : "Hide settings", onTap: () { @@ -423,10 +433,7 @@ class _BuyDomainWidgetState extends ConsumerState { }, ), ), - if (!_settingsHidden) - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + if (!_settingsHidden) SizedBox(height: Util.isDesktop ? 24 : 16), if (!_settingsHidden) if (_dnsRecords.isEmpty) RoundedWhiteContainer( @@ -436,9 +443,10 @@ class _BuyDomainWidgetState extends ConsumerState { Text( "Add DNS records to your domain name", style: STextStyles.w500_12(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], @@ -455,27 +463,25 @@ class _BuyDomainWidgetState extends ConsumerState { (e) => DNSRecordCard( key: ValueKey(e), record: e, - onRemoveTapped: () => setState(() { - _dnsRecords.remove(e); - }), + onRemoveTapped: + () => setState(() { + _dnsRecords.remove(e); + }), ), ), - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), + SizedBox(height: Util.isDesktop ? 16 : 8), SecondaryButton( - label: _dnsRecords.isEmpty - ? "Add DNS record" - : "Add another DNS record", + label: + _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _addRecord, ), ], ), ), - SizedBox( - height: Util.isDesktop ? 24 : 16, - ), + SizedBox(height: Util.isDesktop ? 24 : 16), if (!Util.isDesktop && _settingsHidden) const Spacer(), PrimaryButton( label: "Buy", @@ -483,9 +489,7 @@ class _BuyDomainWidgetState extends ConsumerState { buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: _preRegister, ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + SizedBox(height: Util.isDesktop ? 32 : 16), ], ), ); @@ -524,13 +528,8 @@ class DNSRecordCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "${record.type.name}$_extraInfo", - ), - CustomTextButton( - text: "Remove", - onTap: onRemoveTapped, - ), + Text("${record.type.name}$_extraInfo"), + CustomTextButton(text: "Remove", onTap: onRemoveTapped), ], ), Text(record.getValueString()), diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 1061eb128..89be6129c 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -133,13 +133,14 @@ class _TransferOptionWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, ), ], note: "Transfer ${opName.constructedName}", @@ -236,7 +237,7 @@ class _TransferOptionWidgetState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final coin = ref.read(pWalletCoin(walletId)); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index e438e420c..4fd78fb70 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -147,13 +147,14 @@ class _BuyDomainWidgetState extends ConsumerState { txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( + TxRecipient( address: _address!.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: wallet.cryptoCurrency.fractionDigits, ), + addressType: _address.type, ), ], note: "Update ${opName.constructedName} (${opName.fullname})", diff --git a/lib/pages/paynym/add_new_paynym_follow_view.dart b/lib/pages/paynym/add_new_paynym_follow_view.dart index d74a17429..85e4c3ac7 100644 --- a/lib/pages/paynym/add_new_paynym_follow_view.dart +++ b/lib/pages/paynym/add_new_paynym_follow_view.dart @@ -121,7 +121,7 @@ class _AddNewPaynymFollowViewState await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); final pCodeString = qrResult.rawContent; diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index 25e18becb..96c2e66a6 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -70,60 +70,60 @@ class _AddressDetailsViewState extends ConsumerState { void _showDesktopAddressQrCode() { showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 480, - maxHeight: 400, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + builder: + (context) => DesktopDialog( + maxWidth: 480, + maxHeight: 400, + child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 32), - child: Text( - "Address QR code", - style: STextStyles.desktopH3(context), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Address QR code", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], ), - const DesktopDialogCloseButton(), - ], - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: RepaintBoundary( - key: _qrKey, - child: QR( - data: AddressUtils.buildUriString( - ref.watch(pWalletCoin(widget.walletId)).uriScheme, - address.value, - {}, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: RepaintBoundary( + key: _qrKey, + child: QR( + data: AddressUtils.buildUriString( + ref.watch(pWalletCoin(widget.walletId)).uriScheme, + address.value, + {}, + ), + size: 220, + ), ), - size: 220, ), - ), + ], ), - ], - ), - ), - const SizedBox( - height: 32, + ), + const SizedBox(height: 32), + ], ), - ], - ), - ), + ), ); } @override void initState() { - address = MainDB.instance.isar.addresses - .where() - .idEqualTo(widget.addressId) - .findFirstSync()!; + address = + MainDB.instance.isar.addresses + .where() + .idEqualTo(widget.addressId) + .findFirstSync()!; label = MainDB.instance.getAddressLabelSync(widget.walletId, address.value); Id? id = label?.id; @@ -132,9 +132,10 @@ class _AddressDetailsViewState extends ConsumerState { walletId: widget.walletId, addressString: address.value, value: "", - tags: address.subType == AddressSubType.receiving - ? ["receiving"] - : address.subType == AddressSubType.change + tags: + address.subType == AddressSubType.receiving + ? ["receiving"] + : address.subType == AddressSubType.change ? ["change"] : null, ); @@ -151,43 +152,46 @@ class _AddressDetailsViewState extends ConsumerState { final wallet = ref.watch(pWallets).getWallet(widget.walletId); return ConditionalParent( condition: !isDesktop, - builder: (child) => Background( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.backgroundAppBar, - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - titleSpacing: 0, - title: Text( - "Address details", - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: LayoutBuilder( - builder: (builderContext, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight, - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: child, - ), - ), - ); - }, + builder: + (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Address details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + ), + ), ), ), - ), - ), child: StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -200,9 +204,7 @@ class _AddressDetailsViewState extends ConsumerState { builder: (child) { return Column( children: [ - const SizedBox( - height: 20, - ), + const SizedBox(height: 20), RoundedWhiteContainer( padding: const EdgeInsets.all(24), child: Column( @@ -215,9 +217,10 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), CustomTextButton( @@ -226,19 +229,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ], ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, child: child, ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -247,34 +247,35 @@ class _AddressDetailsViewState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, + color: + Theme.of( + context, + ).extension()!.textSubtitle1, ), ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), RoundedWhiteContainer( padding: EdgeInsets.zero, - borderColor: Theme.of(context) - .extension()! - .backgroundAppBar, - child: ref - .watch(pWallets) - .getWallet(widget.walletId) - .isarTransactionVersion == - 2 - ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) - : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + borderColor: + Theme.of( + context, + ).extension()!.backgroundAppBar, + child: + ref + .watch(pWallets) + .getWallet(widget.walletId) + .isarTransactionVersion == + 2 + ? _AddressDetailsTxV2List( + walletId: widget.walletId, + address: address, + ) + : _AddressDetailsTxList( + walletId: widget.walletId, + address: address, + ), ), ], ), @@ -299,24 +300,16 @@ class _AddressDetailsViewState extends ConsumerState { ), ), ), - if (!isDesktop) - const SizedBox( - height: 16, - ), + if (!isDesktop) const SizedBox(height: 16), DetailItem( title: "Address", detail: address.value, - button: isDesktop - ? IconCopyButton( - data: address.value, - ) - : SimpleCopyButton( - data: address.value, - ), - ), - const _Div( - height: 12, + button: + isDesktop + ? IconCopyButton(data: address.value) + : SimpleCopyButton(data: address.value), ), + const _Div(height: 12), DetailItem( title: "Label", detail: label!.value, @@ -325,59 +318,47 @@ class _AddressDetailsViewState extends ConsumerState { editLabel: 'label', onValueChanged: (value) { MainDB.instance.putAddressLabel( - label!.copyWith( - label: value, - ), + label!.copyWith(label: value), ); }, ), ), - const _Div( - height: 12, - ), - _Tags( - tags: label!.tags, - ), - if (address.derivationPath != null) - const _Div( - height: 12, - ), + const _Div(height: 12), + _Tags(tags: label!.tags), + if (address.derivationPath != null) const _Div(height: 12), if (address.derivationPath != null) DetailItem( title: "Derivation path", detail: address.derivationPath!.value, button: Container(), ), - if (address.type == AddressType.spark) - const _Div( - height: 12, - ), + if (address.type == AddressType.spark) const _Div(height: 12), if (address.type == AddressType.spark) DetailItem( title: "Diversifier", detail: address.derivationIndex.toString(), button: Container(), ), - const _Div( - height: 12, - ), + if (address.type == AddressType.mweb) const _Div(height: 12), + if (address.type == AddressType.mweb) + DetailItem( + title: "Index", + detail: address.derivationIndex.toString(), + button: Container(), + ), + const _Div(height: 12), DetailItem( title: "Type", detail: address.type.readableName, button: Container(), ), - const _Div( - height: 12, - ), + const _Div(height: 12), DetailItem( title: "Sub type", detail: address.subType.prettyName, button: Container(), ), - if (kDebugMode) - const _Div( - height: 12, - ), + if (kDebugMode) const _Div(height: 12), if (kDebugMode) DetailItem( title: "frost secure (kDebugMode)", @@ -385,18 +366,13 @@ class _AddressDetailsViewState extends ConsumerState { button: Container(), ), if (wallet is Bip39HDWallet && !wallet.isViewOnly) - const _Div( - height: 12, - ), + const _Div(height: 12), if (wallet is Bip39HDWallet && !wallet.isViewOnly) AddressPrivateKey( walletId: widget.walletId, address: address, ), - if (!isDesktop) - const SizedBox( - height: 20, - ), + if (!isDesktop) const SizedBox(height: 20), if (!isDesktop) Text( "Transactions", @@ -406,10 +382,7 @@ class _AddressDetailsViewState extends ConsumerState { Theme.of(context).extension()!.textDark3, ), ), - if (!isDesktop) - const SizedBox( - height: 12, - ), + if (!isDesktop) const SizedBox(height: 12), if (!isDesktop) ref .watch(pWallets) @@ -417,13 +390,13 @@ class _AddressDetailsViewState extends ConsumerState { .isarTransactionVersion == 2 ? _AddressDetailsTxV2List( - walletId: widget.walletId, - address: address, - ) + walletId: widget.walletId, + address: address, + ) : _AddressDetailsTxList( - walletId: widget.walletId, - address: address, - ), + walletId: widget.walletId, + address: address, + ), ], ), ); @@ -458,10 +431,9 @@ class _AddressDetailsTxList extends StatelessWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCard( - transaction: txns[index], - walletId: walletId, - ), + itemBuilder: + (_, index) => + TransactionCard(transaction: txns[index], walletId: walletId), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -470,15 +442,14 @@ class _AddressDetailsTxList extends StatelessWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCard( - transaction: e, - walletId: walletId, - ), - ) - .toList(), + children: + query + .findAllSync() + .map( + (e) => + TransactionCard(transaction: e, walletId: walletId), + ) + .toList(), ), ); } @@ -503,40 +474,35 @@ class _AddressDetailsTxV2List extends ConsumerWidget { final walletTxFilter = ref.watch(pWallets).getWallet(walletId).transactionFilterOperation; - final query = - ref.watch(mainDBProvider).isar.transactionV2s.buildQuery( - whereClauses: [ - IndexWhereClause.equalTo( - indexName: 'walletId', - value: [walletId], + final query = ref + .watch(mainDBProvider) + .isar + .transactionV2s + .buildQuery( + whereClauses: [ + IndexWhereClause.equalTo(indexName: 'walletId', value: [walletId]), + ], + filter: FilterGroup.and([ + if (walletTxFilter != null) walletTxFilter, + FilterGroup.or([ + ObjectFilter( + property: 'inputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - filter: FilterGroup.and([ - if (walletTxFilter != null) walletTxFilter, - FilterGroup.or([ - ObjectFilter( - property: 'inputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ObjectFilter( - property: 'outputs', - filter: FilterCondition.contains( - property: "addresses", - value: address.value, - ), - ), - ]), - ]), - sortBy: [ - const SortProperty( - property: "timestamp", - sort: Sort.desc, + ), + ObjectFilter( + property: 'outputs', + filter: FilterCondition.contains( + property: "addresses", + value: address.value, ), - ], - ); + ), + ]), + ]), + sortBy: [const SortProperty(property: "timestamp", sort: Sort.desc)], + ); final count = query.countSync(); @@ -546,9 +512,8 @@ class _AddressDetailsTxV2List extends ConsumerWidget { return ListView.separated( shrinkWrap: true, primary: false, - itemBuilder: (_, index) => TransactionCardV2( - transaction: txns[index], - ), + itemBuilder: + (_, index) => TransactionCardV2(transaction: txns[index]), separatorBuilder: (_, __) => const _Div(height: 1), itemCount: count, ); @@ -557,14 +522,11 @@ class _AddressDetailsTxV2List extends ConsumerWidget { padding: EdgeInsets.zero, child: Column( mainAxisSize: MainAxisSize.min, - children: query - .findAllSync() - .map( - (e) => TransactionCardV2( - transaction: e, - ), - ) - .toList(), + children: + query + .findAllSync() + .map((e) => TransactionCardV2(transaction: e)) + .toList(), ), ); } @@ -575,10 +537,7 @@ class _AddressDetailsTxV2List extends ConsumerWidget { } class _Div extends StatelessWidget { - const _Div({ - super.key, - required this.height, - }); + const _Div({super.key, required this.height}); final double height; @@ -591,18 +550,13 @@ class _Div extends StatelessWidget { width: double.infinity, ); } else { - return SizedBox( - height: height, - ); + return SizedBox(height: height); } } } class _Tags extends StatelessWidget { - const _Tags({ - super.key, - required this.tags, - }); + const _Tags({super.key, required this.tags}); final List? tags; @@ -615,10 +569,7 @@ class _Tags extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - "Tags", - style: STextStyles.itemSubtitle(context), - ), + Text("Tags", style: STextStyles.itemSubtitle(context)), Container(), // SimpleEditButton( // onPressedOverride: () { @@ -627,29 +578,20 @@ class _Tags extends StatelessWidget { // ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), tags != null && tags!.isNotEmpty ? Wrap( - spacing: 10, - runSpacing: 10, - children: tags! - .map( - (e) => AddressTag( - tag: e, - ), - ) - .toList(), - ) + spacing: 10, + runSpacing: 10, + children: tags!.map((e) => AddressTag(tag: e)).toList(), + ) : Text( - "Tags will appear here", - style: STextStyles.w500_14(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle3, - ), + "Tags will appear here", + style: STextStyles.w500_14(context).copyWith( + color: + Theme.of(context).extension()!.textSubtitle3, ), + ), ], ), ); diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 9c8abcb6c..fe44acc9b 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -28,6 +28,7 @@ import '../../utilities/assets.dart'; import '../../utilities/clipboard_interface.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/derive_path_type_enum.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; @@ -36,6 +37,7 @@ import '../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../widgets/background.dart'; @@ -74,6 +76,7 @@ class _ReceiveViewState extends ConsumerState { late final ClipboardInterface clipboard; late final bool _supportsSpark; late final bool _showMultiType; + late bool supportsMweb; int _currentIndex = 0; @@ -202,6 +205,31 @@ class _ReceiveViewState extends ConsumerState { } } + Future
_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; @@ -209,12 +237,17 @@ class _ReceiveViewState extends ConsumerState { clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); _supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { _showMultiType = false; } else { _showMultiType = _supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -231,6 +264,10 @@ class _ReceiveViewState extends ConsumerState { (e) => e != wallet.info.mainAddressType, ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -252,6 +289,9 @@ class _ReceiveViewState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() @@ -285,6 +325,57 @@ class _ReceiveViewState extends ConsumerState { final ticker = widget.tokenContract?.symbol ?? coin.ticker; + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (_showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -302,7 +393,8 @@ class _ReceiveViewState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || _supportsSpark); + canGen = + (wallet is MultiAddressInterface || _supportsSpark || supportsMweb); } return Background( @@ -590,7 +682,11 @@ class _ReceiveViewState extends ConsumerState { SecondaryButton( label: "Generate new address", onPressed: - _supportsSpark && + supportsMweb && + _walletAddressTypes[_currentIndex] == + AddressType.mweb + ? generateNewMwebAddress + : _supportsSpark && _walletAddressTypes[_currentIndex] == AddressType.spark ? generateNewSparkAddress diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index d34306cf4..220d0e04f 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -140,7 +140,7 @@ class _ConfirmTransactionViewState } else { if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints == null) { txDataFuture = wallet.confirmSend(txData: widget.txData); } else { @@ -150,7 +150,7 @@ class _ConfirmTransactionViewState } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.confirmSendSpark(txData: widget.txData); break; } @@ -346,7 +346,7 @@ class _ConfirmTransactionViewState if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (widget.txData.sparkMints != null) { fee = widget.txData.sparkMints! .map((e) => e.fee!) @@ -360,7 +360,7 @@ class _ConfirmTransactionViewState } break; - case FiroType.spark: + case BalanceType.private: fee = widget.txData.fee; amountWithoutChange = (widget.txData.amountWithoutChange ?? diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index 22c290036..4b1014158 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -83,7 +83,14 @@ class _FrostSendViewState extends ConsumerState { final recipients = recipientWidgetIndexes .map((i) => ref.read(pRecipient(i).state).state) - .map((e) => (address: e!.address, amount: e!.amount!, isChange: false)) + .map( + (e) => TxRecipient( + address: e!.address, + amount: e.amount!, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(e.address)!, + ), + ) .toList(growable: false); final txData = await wallet.frostCreateSignConfig( @@ -230,7 +237,7 @@ class _FrostSendViewState extends ConsumerState { ), ) && (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public + ? ref.watch(publicPrivateBalanceStateProvider) == BalanceType.public : true); return ConditionalParent( diff --git a/lib/pages/send_view/frost_ms/recipient.dart b/lib/pages/send_view/frost_ms/recipient.dart index 36122962c..150eecb0b 100644 --- a/lib/pages/send_view/frost_ms/recipient.dart +++ b/lib/pages/send_view/frost_ms/recipient.dart @@ -121,7 +121,7 @@ class _RecipientState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); Logging.instance.d("qrResult content: ${qrResult.rawContent}"); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart index ade993b22..e5f6581a1 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_1b.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import '../../../../frost_route_generator.dart'; +import '../../../../models/input.dart'; import '../../../../models/isar/models/isar_models.dart'; import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/frost_wallet/frost_wallet_providers.dart'; @@ -60,9 +61,9 @@ class _FrostSendStep1bState extends ConsumerState { } final config = configFieldController.text; - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final data = Frost.extractDataFromSignConfig( signConfig: config, @@ -70,28 +71,38 @@ class _FrostSendStep1bState extends ConsumerState { serializedKeys: (await wallet.getSerializedKeys())!, ); - final utxos = await ref - .read(mainDBProvider) - .getUTXOs(wallet.walletId) - .filter() - .anyOf( - data.inputs, - (q, e) => q - .txidEqualTo(Format.uint8listToString(e.hash)) - .and() - .valueEqualTo(e.value) - .and() - .voutEqualTo(e.vout), - ) - .findAll(); + final utxos = + await ref + .read(mainDBProvider) + .getUTXOs(wallet.walletId) + .filter() + .anyOf( + data.inputs, + (q, e) => q + .txidEqualTo(Format.uint8listToString(e.hash)) + .and() + .valueEqualTo(e.value) + .and() + .voutEqualTo(e.vout), + ) + .findAll(); // TODO add more data from 'data' and display to user ? ref.read(pFrostTxData.notifier).state = TxData( frostMSConfig: config, - recipients: data.recipients - .map((e) => (address: e.address, amount: e.amount, isChange: false)) - .toList(), - utxos: utxos.toSet(), + recipients: + data.recipients + .map( + (e) => TxRecipient( + address: e.address, + amount: e.amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(e.address)!, + ), + ) + .toList(), + utxos: utxos.map((e) => StandardInput(e)).toSet(), ); final attemptSignRes = await wallet.frostAttemptSignConfig( @@ -112,11 +123,12 @@ class _FrostSendStep1bState extends ConsumerState { if (mounted) { await showDialog( context: context, - builder: (_) => StackOkDialog( - title: "Import and attempt sign config failed", - message: e.toString(), - desktopPopRootNavigator: Util.isDesktop, - ), + builder: + (_) => StackOkDialog( + title: "Import and attempt sign config failed", + message: e.toString(), + desktopPopRootNavigator: Util.isDesktop, + ), ); } } finally { @@ -128,9 +140,9 @@ class _FrostSendStep1bState extends ConsumerState { void initState() { configFieldController = TextEditingController(); configFocusNode = FocusNode(); - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(pFrostMyName.state).state = wallet.frostInfo.myName; }); @@ -152,9 +164,7 @@ class _FrostSendStep1bState extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), + const FrostStepUserSteps(userSteps: info), const SizedBox(height: 20), FrostStepField( controller: configFieldController, @@ -169,11 +179,10 @@ class _FrostSendStep1bState extends ConsumerState { }, ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( - label: "I have verified that everyone has imported he config and" + label: + "I have verified that everyone has imported he config and" " is ready to sign", onChanged: (value) { setState(() { @@ -181,9 +190,7 @@ class _FrostSendStep1bState extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Start signing", enabled: !_configEmpty && _userVerifyContinue, diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index 6ce475a0b..f3ddc90cc 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -51,9 +51,9 @@ class _FrostSendStep3State extends ConsumerState { @override void initState() { - final wallet = ref.read(pWallets).getWallet( - ref.read(pFrostScaffoldArgs)!.walletId!, - ) as BitcoinFrostWallet; + final wallet = + ref.read(pWallets).getWallet(ref.read(pFrostScaffoldArgs)!.walletId!) + as BitcoinFrostWallet; final frostInfo = wallet.frostInfo; @@ -62,12 +62,13 @@ class _FrostSendStep3State extends ConsumerState { myIndex = frostInfo.participants.indexOf(frostInfo.myName); myShare = ref.read(pFrostContinueSignData.state).state!.share; - participantsWithoutMe = frostInfo.participants - .toSet() - .intersection( - ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet(), - ) - .toList(); + participantsWithoutMe = + frostInfo.participants + .toSet() + .intersection( + ref.read(pFrostSelectParticipantsUnordered.state).state!.toSet(), + ) + .toList(); participantsWithoutMe.remove(myName); @@ -98,46 +99,28 @@ class _FrostSendStep3State extends ConsumerState { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const FrostStepUserSteps( - userSteps: info, - ), - const SizedBox( - height: 12, - ), + const FrostStepUserSteps(userSteps: info), + const SizedBox(height: 12), DetailItem( title: "My name", detail: myName, - button: Util.isDesktop - ? IconCopyButton( - data: myName, - ) - : SimpleCopyButton( - data: myName, - ), - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myName) + : SimpleCopyButton(data: myName), ), + const SizedBox(height: 12), DetailItem( title: "My share", detail: myShare, - button: Util.isDesktop - ? IconCopyButton( - data: myShare, - ) - : SimpleCopyButton( - data: myShare, - ), - ), - const SizedBox( - height: 12, - ), - FrostQrDialogPopupButton( - data: myShare, - ), - const SizedBox( - height: 12, + button: + Util.isDesktop + ? IconCopyButton(data: myShare) + : SimpleCopyButton(data: myShare), ), + const SizedBox(height: 12), + FrostQrDialogPopupButton(data: myShare), + const SizedBox(height: 12), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -158,9 +141,7 @@ class _FrostSendStep3State extends ConsumerState { ], ), if (!Util.isDesktop) const Spacer(), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), CheckboxTextButton( label: "I have verified that everyone has my share", onChanged: (value) { @@ -169,12 +150,11 @@ class _FrostSendStep3State extends ConsumerState { }); }, ), - const SizedBox( - height: 12, - ), + const SizedBox(height: 12), PrimaryButton( label: "Generate transaction", - enabled: _userVerifyContinue && + enabled: + _userVerifyContinue && !fieldIsEmptyFlags.fold(false, (v, e) => v |= e), onPressed: () async { // collect Share strings @@ -206,23 +186,21 @@ class _FrostSendStep3State extends ConsumerState { final inputTotal = Amount( rawValue: txData.utxos! - .map((e) => BigInt.from(e.value)) + .map((e) => e.value) .reduce((v, e) => v += e), fractionDigits: fractionDigits, ); final outputTotal = Amount( - rawValue: - tx.outputs.map((e) => e.value).reduce((v, e) => v += e), + rawValue: tx.outputs + .map((e) => e.value) + .reduce((v, e) => v += e), fractionDigits: fractionDigits, ); ref.read(pFrostTxData.state).state = txData.copyWith( raw: rawTx, fee: inputTotal - outputTotal, - frostSigners: [ - myName, - ...participantsWithoutMe, - ], + frostSigners: [myName, ...participantsWithoutMe], ); ref.read(pFrostCreateCurrentStep.state).state = 4; @@ -233,14 +211,15 @@ class _FrostSendStep3State extends ConsumerState { .routeName, ); } catch (e, s) { - Logging.instance.f("$e\n$s", error: e, stackTrace: s,); + Logging.instance.f("$e\n$s", error: e, stackTrace: s); if (context.mounted) { return await showDialog( context: context, - builder: (_) => const FrostErrorDialog( - title: "Failed to complete signing process", - ), + builder: + (_) => const FrostErrorDialog( + title: "Failed to complete signing process", + ), ); } } diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 54a2f2f8c..9ac52806f 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -19,6 +19,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; +import '../../models/input.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; import '../../models/send_view_auto_fill_data.dart'; @@ -52,6 +53,7 @@ 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/mweb_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'; @@ -73,7 +75,7 @@ import '../address_book_views/address_book_view.dart'; import '../coin_control/coin_control_view.dart'; import 'confirm_transaction_view.dart'; import 'sub_widgets/building_transaction_dialog.dart'; -import 'sub_widgets/firo_balance_selection_sheet.dart'; +import 'sub_widgets/dual_balance_selection_sheet.dart'; import 'sub_widgets/transaction_fee_selection_sheet.dart'; class SendView extends ConsumerStatefulWidget { @@ -141,7 +143,7 @@ class _SendViewState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Set selectedUTXOs = {}; + Set selectedUTXOs = {}; void _applyUri(PaymentUriData paymentData) { try { @@ -268,7 +270,7 @@ class _SendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), @@ -479,7 +481,8 @@ class _SendViewState extends ConsumerState { ref.read(pIsExchangeAddress.state).state = (coin as Firo) .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -508,12 +511,12 @@ class _SendViewState extends ConsumerState { if (isFiro) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (cachedFiroPublicFees[amount] != null) { return cachedFiroPublicFees[amount]!; } break; - case FiroType.spark: + case BalanceType.private: if (cachedFiroSparkFees[amount] != null) { return cachedFiroSparkFees[amount]!; } @@ -572,14 +575,14 @@ class _SendViewState extends ConsumerState { final firoWallet = wallet as FiroWallet; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: fee = await firoWallet.estimateFeeFor(amount, feeRate); cachedFiroPublicFees[amount] = ref .read(pAmountFormatter(coin)) .format(fee, withUnitName: true, indicatePrecisionLoss: false); return cachedFiroPublicFees[amount]!; - case FiroType.spark: + case BalanceType.private: fee = await firoWallet.estimateFeeForSpark(amount); cachedFiroSparkFees[amount] = ref .read(pAmountFormatter(coin)) @@ -604,13 +607,16 @@ class _SendViewState extends ConsumerState { final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if (isFiro) { + if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.spark: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + case BalanceType.private: + availableBalance = + isFiro + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -691,7 +697,7 @@ class _SendViewState extends ConsumerState { isSpark: wallet is FiroWallet && ref.read(publicPrivateBalanceStateProvider.state).state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -713,10 +719,11 @@ class _SendViewState extends ConsumerState { txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -731,7 +738,7 @@ class _SendViewState extends ConsumerState { ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -755,7 +762,13 @@ class _SendViewState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeMobileStateProvider), satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -768,14 +781,22 @@ class _SendViewState extends ConsumerState { } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( recipients: ref.read(pValidSparkSendToAddress) ? null : [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) @@ -792,11 +813,42 @@ class _SendViewState extends ConsumerState { ); break; } + } else if (wallet is MwebInterface && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSendMweb( + txData: TxData( + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee.value ? customFeeRate : null, + + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, + ), + ); } else { final memo = coin is Stellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], memo: memo, feeRateType: ref.read(feeRateTypeMobileStateProvider), satsPerVByte: isCustomFee.value ? customFeeRate : null, @@ -905,7 +957,10 @@ class _SendViewState extends ConsumerState { } } - String _getSendAllTitle(bool showCoinControl, Set selectedUTXOs) { + String _getSendAllTitle( + bool showCoinControl, + Set selectedUTXOs, + ) { if (showCoinControl && selectedUTXOs.isNotEmpty) { return "Send all selected"; } @@ -913,8 +968,8 @@ class _SendViewState extends ConsumerState { return "Send all ${coin.ticker}"; } - Amount _selectedUtxosAmount(Set utxos) => Amount( - rawValue: utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e), + Amount _selectedUtxosAmount(Set utxos) => Amount( + rawValue: utxos.map((e) => e.value).reduce((v, e) => v += e), fractionDigits: ref.read(pWalletCoin(walletId)).fractionDigits, ); @@ -923,14 +978,17 @@ class _SendViewState extends ConsumerState { if (showCoinControl && selectedUTXOs.isNotEmpty) { amount = _selectedUtxosAmount(selectedUTXOs); - } else if (isFiro) { + } else if (isFiro || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; break; - case FiroType.spark: - amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; + case BalanceType.private: + amount = + isFiro + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { @@ -1146,54 +1204,56 @@ class _SendViewState extends ConsumerState { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final wallet = ref.watch(pWallets).getWallet(walletId); final String locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale), ); + final balType = ref.watch(publicPrivateBalanceStateProvider); + + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; + final showCoinControl = - wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( (value) => value.enableCoinControl, ), ) && - (coin is Firo - ? ref.watch(publicPrivateBalanceStateProvider) == FiroType.public - : true); - - if (isFiro) { - final isExchangeAddress = ref.watch(pIsExchangeAddress); + ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && + balType == BalanceType.public; - ref.listen(publicPrivateBalanceStateProvider, (previous, next) { - selectedUTXOs = {}; + final isExchangeAddress = ref.watch(pIsExchangeAddress); - if (ref.read(pSendAmount) == null) { - setState(() { - _calculateFeesFuture = calculateFees( - 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), - ); - }); - } else { - setState(() { - _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); - }); - } + ref.listen(publicPrivateBalanceStateProvider, (previous, next) { + selectedUTXOs = {}; - if (previous != next && - next == FiroType.spark && - isExchangeAddress && - !_isFiroExWarningDisplayed) { - _isFiroExWarningDisplayed = true; - WidgetsBinding.instance.addPostFrameCallback( - (_) => showFiroExchangeAddressWarning( - context, - () => _isFiroExWarningDisplayed = false, - ), + if (ref.read(pSendAmount) == null) { + setState(() { + _calculateFeesFuture = calculateFees( + 0.toAmountAsRaw(fractionDigits: coin.fractionDigits), ); - } - }); - } + }); + } else { + setState(() { + _calculateFeesFuture = calculateFees(ref.read(pSendAmount)!); + }); + } + + if (previous != next && + next == BalanceType.private && + isExchangeAddress && + !_isFiroExWarningDisplayed) { + _isFiroExWarningDisplayed = true; + WidgetsBinding.instance.addPostFrameCallback( + (_) => showFiroExchangeAddressWarning( + context, + () => _isFiroExWarningDisplayed = false, + ), + ); + } + }); // add listener for epic cash to strip http:// and https:// prefixes if the address also ocntains an @ symbol (indicating an epicbox address) if (coin is Epiccash) { @@ -1294,9 +1354,9 @@ class _SendViewState extends ConsumerState { // const SizedBox( // height: 2, // ), - if (isFiro) + if (isFiro || isMwebEnabled) Text( - "${ref.watch(publicPrivateBalanceStateProvider.state).state.name.capitalize()} balance", + "${balType.name.capitalize()} balance", style: STextStyles.label( context, ).copyWith(fontSize: 10), @@ -1314,14 +1374,9 @@ class _SendViewState extends ConsumerState { Builder( builder: (context) { final Amount amount; - if (isFiro) { - switch (ref - .watch( - publicPrivateBalanceStateProvider - .state, - ) - .state) { - case FiroType.public: + if (showPrivateBalance) { + switch (balType) { + case BalanceType.public: amount = ref .read( @@ -1332,13 +1387,17 @@ class _SendViewState extends ConsumerState { .spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref .read( - pWalletBalanceTertiary( - walletId, - ), + isMwebEnabled + ? pWalletBalanceSecondary( + walletId, + ) + : pWalletBalanceTertiary( + walletId, + ), ) .spendable; break; @@ -1719,15 +1778,17 @@ class _SendViewState extends ConsumerState { } }, ), - if (isFiro) const SizedBox(height: 12), - if (isFiro) + if (isFiro || isMwebEnabled) + const SizedBox(height: 12), + if (isFiro || isMwebEnabled) Text( "Send from", style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - if (isFiro) const SizedBox(height: 8), - if (isFiro) + if (isFiro || isMwebEnabled) + const SizedBox(height: 8), + if (isFiro || isMwebEnabled) Stack( children: [ TextField( @@ -1761,7 +1822,7 @@ class _SendViewState extends ConsumerState { ), ), builder: - (_) => FiroBalanceSelectionSheet( + (_) => DualBalanceSelectionSheet( walletId: walletId, ), ); @@ -1789,7 +1850,7 @@ class _SendViewState extends ConsumerState { .state, ) .state) { - case FiroType.public: + case BalanceType.public: amount = ref .watch( @@ -1799,13 +1860,17 @@ class _SendViewState extends ConsumerState { ) .spendable; break; - case FiroType.spark: + case BalanceType.private: amount = ref .watch( - pWalletBalanceTertiary( - walletId, - ), + isFiro + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), ) .spendable; break; @@ -2064,13 +2129,20 @@ class _SendViewState extends ConsumerState { walletId, CoinControlViewType.use, amount, - selectedUTXOs, + selectedUTXOs + .map((e) => e.utxo) + .toSet(), ), ); if (result is Set) { setState(() { - selectedUTXOs = result; + selectedUTXOs = + result + .map( + (e) => StandardInput(e), + ) + .toSet(); }); } } @@ -2229,7 +2301,7 @@ class _SendViewState extends ConsumerState { .state, ) .state != - FiroType.public + BalanceType.public ? null : _onFeeSelectPressed, child: @@ -2240,7 +2312,7 @@ class _SendViewState extends ConsumerState { .state, ) .state != - FiroType.public) + BalanceType.public) ? Row( children: [ FutureBuilder( diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart similarity index 83% rename from lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart rename to lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart index 87c6da542..fbd9ad996 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/dual_balance_selection_sheet.dart @@ -11,26 +11,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../providers/providers.dart'; import '../../../providers/wallet/public_private_balance_state_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount_formatter.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; -import '../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../wallets/crypto_currency/coins/firo.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; -class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { - const FiroBalanceSelectionSheet({super.key, required this.walletId}); +class DualBalanceSelectionSheet extends ConsumerStatefulWidget { + const DualBalanceSelectionSheet({super.key, required this.walletId}); final String walletId; @override - ConsumerState createState() => + ConsumerState createState() => _FiroBalanceSelectionSheetState(); } class _FiroBalanceSelectionSheetState - extends ConsumerState { + extends ConsumerState { late final String walletId; @override @@ -43,12 +43,7 @@ class _FiroBalanceSelectionSheetState Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final wallet = ref.watch( - pWallets.select((value) => value.getWallet(walletId)), - ); - final firoWallet = wallet as FiroWallet; - - final coin = wallet.info.coin; + final coin = ref.watch(pWalletCoin(walletId)); return Container( decoration: BoxDecoration( @@ -90,9 +85,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.spark) { + if (state != BalanceType.private) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; } Navigator.of(context).pop(); }, @@ -112,7 +107,7 @@ class _FiroBalanceSelectionSheetState Theme.of(context) .extension()! .radioButtonIconEnabled, - value: FiroType.spark, + value: BalanceType.private, groupValue: ref .watch( @@ -125,7 +120,7 @@ class _FiroBalanceSelectionSheetState .read( publicPrivateBalanceStateProvider.state, ) - .state = FiroType.spark; + .state = BalanceType.private; Navigator.of(context).pop(); }, @@ -141,7 +136,7 @@ class _FiroBalanceSelectionSheetState // Row( // children: [ Text( - "Spark balance", + "Private balance", style: STextStyles.titleBold12(context), textAlign: TextAlign.left, ), @@ -150,9 +145,16 @@ class _FiroBalanceSelectionSheetState ref .watch(pAmountFormatter(coin)) .format( - firoWallet - .info - .cachedBalanceTertiary + ref + .watch( + coin is Firo + ? pWalletBalanceTertiary( + walletId, + ) + : pWalletBalanceSecondary( + walletId, + ), + ) .spendable, ), style: STextStyles.itemSubtitle(context), @@ -173,9 +175,9 @@ class _FiroBalanceSelectionSheetState onTap: () { final state = ref.read(publicPrivateBalanceStateProvider.state).state; - if (state != FiroType.public) { + if (state != BalanceType.public) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; } Navigator.of(context).pop(); }, @@ -194,7 +196,7 @@ class _FiroBalanceSelectionSheetState Theme.of(context) .extension()! .radioButtonIconEnabled, - value: FiroType.public, + value: BalanceType.public, groupValue: ref .watch( @@ -207,7 +209,7 @@ class _FiroBalanceSelectionSheetState .read( publicPrivateBalanceStateProvider.state, ) - .state = FiroType.public; + .state = BalanceType.public; Navigator.of(context).pop(); }, ), @@ -231,7 +233,9 @@ class _FiroBalanceSelectionSheetState ref .watch(pAmountFormatter(coin)) .format( - firoWallet.info.cachedBalance.spendable, + ref + .watch(pWalletBalance(walletId)) + .spendable, ), style: STextStyles.itemSubtitle(context), textAlign: TextAlign.left, 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 2257af491..3ec7048a3 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 @@ -97,11 +97,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -134,11 +134,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -170,11 +170,11 @@ class _TransactionFeeSelectionSheetState } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart index 635df7e89..fdf78136e 100644 --- a/lib/pages/send_view/token_send_view.dart +++ b/lib/pages/send_view/token_send_view.dart @@ -151,7 +151,7 @@ class _TokenSendViewState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); // Future.delayed( // const Duration(seconds: 2), @@ -491,7 +491,15 @@ class _TokenSendViewState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], feeRateType: ref.read(feeRateTypeMobileStateProvider), note: noteController.text, ethEIP1559Fee: ethFee, 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 27b50796d..ca5b825ad 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 @@ -377,7 +377,7 @@ class _AddEditNodeViewState extends ConsumerState { } } else { try { - final result = await ref.read(pBarcodeScanner).scan(); + final result = await ref.read(pBarcodeScanner).scan(context: context); await _processQrData(result.rawContent); } on PlatformException catch (e, s) { if (mounted) { diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart index 506f1167c..27e7cc63c 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart @@ -33,13 +33,16 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/coins/epiccash.dart'; +import '../../../../wallets/crypto_currency/coins/litecoin.dart'; import '../../../../wallets/crypto_currency/coins/monero.dart'; +import '../../../../wallets/crypto_currency/coins/salvium.dart'; import '../../../../wallets/crypto_currency/coins/wownero.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/impl/epiccash_wallet.dart'; -import '../../../../wallets/wallet/impl/monero_wallet.dart'; -import '../../../../wallets/wallet/impl/wownero_wallet.dart'; +import '../../../../wallets/wallet/impl/salvium_wallet.dart'; +import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../widgets/animated_text.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -269,7 +272,13 @@ class _WalletNetworkSettingsViewState final coin = ref.read(pWalletCoin(widget.walletId)); - if (coin is Monero || coin is Wownero || coin is Epiccash) { + // TODO: handle isMwebEnabled toggled + if (coin is Monero || + coin is Wownero || + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.read(pWalletInfo(widget.walletId)).isMwebEnabled)) { _blocksRemainingSubscription = eventBus.on().listen( (event) async { if (event.walletId == widget.walletId) { @@ -329,16 +338,23 @@ class _WalletNetworkSettingsViewState final coin = ref.watch(pWalletCoin(widget.walletId)); - if (coin is Monero) { + if (coin is Salvium) { final double highestPercent = - (ref.read(pWallets).getWallet(widget.walletId) as MoneroWallet) + (ref.read(pWallets).getWallet(widget.walletId) as SalviumWallet) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); } - } else if (coin is Wownero) { + } else if (coin is Monero || coin is Wownero) { final double highestPercent = - (ref.watch(pWallets).getWallet(widget.walletId) as WowneroWallet) + (ref.read(pWallets).getWallet(widget.walletId) as LibMoneroWallet) + .highestPercentCached; + if (_percent < highestPercent) { + _percent = highestPercent.clamp(0.0, 1.0); + } + } else if (coin is Litecoin) { + final double highestPercent = + (ref.watch(pWallets).getWallet(widget.walletId) as MwebInterface) .highestPercentCached; if (_percent < highestPercent) { _percent = highestPercent.clamp(0.0, 1.0); @@ -637,7 +653,14 @@ class _WalletNetworkSettingsViewState ), if (coin is Monero || coin is Wownero || - coin is Epiccash) + coin is Epiccash || + coin is Salvium || + (coin is Litecoin && + ref.watch( + pWalletInfo( + widget.walletId, + ).select((s) => s.isMwebEnabled), + ))) Text( " (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})", style: STextStyles.syncPercent( diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index a1f9f19dc..7792fb2e1 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -8,6 +8,8 @@ * */ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -23,12 +25,15 @@ import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../wallets/wallet/intermediate/lib_salvium_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_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/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; @@ -53,6 +58,7 @@ class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { class _WalletSettingsWalletSettingsViewState extends ConsumerState { late final DSBController _switchController; + late final DSBController _switchControllerMwebToggle; bool _switchDuressToggleLock = false; // Mutex. Future _switchDuressToggled() async { @@ -135,6 +141,71 @@ class _WalletSettingsWalletSettingsViewState } } + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return StackDialog( + title: "Notice", + message: + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", + leftButton: SecondaryButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + rightButton: PrimaryButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool value) async { + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: value}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != value) { + _switchControllerMwebToggle.activate?.call(); + } + } + } + Future _updateAddressReuse(bool shouldReuse) async { await ref .read(pWalletInfo(widget.walletId)) @@ -153,6 +224,7 @@ class _WalletSettingsWalletSettingsViewState @override void initState() { _switchController = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -304,6 +376,56 @@ class _WalletSettingsWalletSettingsViewState ), ), ), + if (wallet is MwebInterface) const SizedBox(height: 8), + if (wallet is MwebInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: _switchMwebToggleToggled, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable MWEB", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: IgnorePointer( + child: DraggableSwitchButton( + isOn: + ref.watch( + pWalletInfo( + widget.walletId, + ).select( + (value) => value.otherData, + ), + )[WalletInfoKeys.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + ], + ), + ), + ), + ), if (!ref.watch(pDuress)) const SizedBox(height: 8), if (!ref.watch(pDuress)) RoundedWhiteContainer( diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index bbe90033e..120b99b25 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -23,7 +23,7 @@ import '../../../utilities/text_styles.dart'; import '../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; -enum _BalanceType { available, full, sparkAvailable, sparkFull } +enum _BalanceType { available, full, privateAvailable, privateFull } class WalletBalanceToggleSheet extends ConsumerWidget { const WalletBalanceToggleSheet({super.key, required this.walletId}); @@ -35,7 +35,10 @@ class WalletBalanceToggleSheet extends ConsumerWidget { final maxHeight = MediaQuery.of(context).size.height * 0.90; final coin = ref.watch(pWalletCoin(walletId)); - final isFiro = coin is Firo; + final isMweb = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final hasPrivate = isMweb || coin is Firo; final balance = ref.watch(pWalletBalance(walletId)); @@ -45,16 +48,19 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ? _BalanceType.available : _BalanceType.full; - Balance? balanceTertiary; - if (isFiro) { - balanceTertiary = ref.watch(pWalletBalanceTertiary(walletId)); + Balance? balancePrivate; + if (hasPrivate) { + balancePrivate = + isMweb + ? ref.watch(pWalletBalanceSecondary(walletId)) + : ref.watch(pWalletBalanceTertiary(walletId)); if (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.spark) { + BalanceType.private) { _bal = _bal == _BalanceType.available - ? _BalanceType.sparkAvailable - : _BalanceType.sparkFull; + ? _BalanceType.privateAvailable + : _BalanceType.privateFull; } } @@ -102,21 +108,21 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), const SizedBox(height: 24), BalanceSelector( - title: "Available${isFiro ? " public" : ""} balance", + title: "Available${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.available, @@ -124,70 +130,70 @@ class WalletBalanceToggleSheet extends ConsumerWidget { ), const SizedBox(height: 12), BalanceSelector( - title: "Full${isFiro ? " public" : ""} balance", + title: "Full${hasPrivate ? " public" : ""} balance", coin: coin, balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; Navigator.of(context).pop(); }, value: _BalanceType.full, groupValue: _bal, ), - if (balanceTertiary != null) const SizedBox(height: 12), - if (balanceTertiary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Available Spark balance", + title: "Available Private balance", coin: coin, - balance: balanceTertiary.spendable, + balance: balancePrivate.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkAvailable, + value: _BalanceType.privateAvailable, groupValue: _bal, ), - if (balanceTertiary != null) const SizedBox(height: 12), - if (balanceTertiary != null) + if (balancePrivate != null) const SizedBox(height: 12), + if (balancePrivate != null) BalanceSelector( - title: "Full Spark balance", + title: "Full Private balance", coin: coin, - balance: balanceTertiary.total, + balance: balancePrivate.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, onChanged: (_) { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; Navigator.of(context).pop(); }, - value: _BalanceType.sparkFull, + value: _BalanceType.privateFull, groupValue: _bal, ), const SizedBox(height: 40), 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 7661bffdd..68e0123bc 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -85,10 +85,6 @@ class WalletSummaryInfo extends ConsumerWidget { ); } - final priceTuple = ref.watch( - priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin)), - ); - final _showAvailable = ref.watch(walletBalanceToggleStateProvider) == WalletBalanceToggleState.available; @@ -98,18 +94,21 @@ class WalletSummaryInfo extends ConsumerWidget { final bool toggleBalance; - if (coin is Firo) { + if (coin is Firo || ref.watch(pWalletInfo(walletId)).isMwebEnabled) { toggleBalance = false; final type = ref.watch(publicPrivateBalanceStateProvider.state).state; title = "${_showAvailable ? "Available" : "Full"} ${type.name.capitalize()} balance"; switch (type) { - case FiroType.spark: - final balance = ref.watch(pWalletBalanceTertiary(walletId)); + case BalanceType.private: + final balance = + coin is Firo + ? ref.watch(pWalletBalanceTertiary(walletId)) + : ref.watch(pWalletBalanceSecondary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; 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 c6ff6c0d5..c2a7e9adf 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 @@ -1852,147 +1852,306 @@ class _TransactionV2DetailsViewState isDesktop ? const _Divider() : const SizedBox(height: 12), - RoundedWhiteContainer( - padding: - isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - ConditionalParent( - condition: !isDesktop, - builder: - (child) => Row( - children: [ - Expanded(child: child), - SimpleCopyButton( - data: _transaction.txid, + + _transaction.txid.startsWith("mweb_outputId_") && + _transaction.subType == + TransactionSubType.mweb + ? RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: _transaction + .txid + .replaceFirst( + "mweb_outputId_", + "", + ), + ), + ], ), - ], + child: Text( + "MWEB Output ID", + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), ), - child: Text( - "Transaction ID", - style: - isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), - ), + ), + const SizedBox(height: 8), + SelectableText( + _transaction.txid.replaceFirst( + "mweb_outputId_", + "", + ), + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: + Theme.of(context) + .extension< + StackColors + >()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // const SizedBox(height: 8), + // if (coin is Litecoin && + // coin.network == + // CryptoCurrencyNetwork + // .main) + // CustomTextButton( + // text: + // "Open in block explorer", + // onTap: () async { + // final uri = + // getBlockExplorerTransactionUrlFor( + // coin: coin, + // txid: _transaction + // .txid + // .replaceFirst( + // "mweb_outputId_", + // "", + // ), + // ); + // + // if (ref + // .read( + // prefsChangeNotifierProvider, + // ) + // .hideBlockExplorerWarning == + // false) { + // final shouldContinue = + // await showExplorerWarning( + // "${uri.scheme}://${uri.host}", + // ); + // + // if (!shouldContinue) { + // return; + // } + // } + // try { + // await launchUrl( + // uri, + // mode: + // LaunchMode + // .externalApplication, + // ); + // } catch (_) { + // if (context.mounted) { + // unawaited( + // showDialog( + // context: context, + // builder: + // ( + // _, + // ) => StackOkDialog( + // title: + // "Could not open in block explorer", + // message: + // "Failed to open \"${uri.toString()}\"", + // ), + // ), + // ); + // } + // } + // }, + // ), + ], ), - 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< - StackColors - >()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, - ), + ), + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid + .replaceFirst( + "mweb_outputId_", + "", + ), ), - if (coin is! Epiccash) - const SizedBox(height: 8), - if (coin is! Epiccash) - CustomTextButton( - text: "Open in block explorer", - onTap: () async { - final uri = - getBlockExplorerTransactionUrlFor( - coin: coin, - txid: _transaction.txid, - ); - - if (ref - .read( - prefsChangeNotifierProvider, + ], + ), + ) + : RoundedWhiteContainer( + padding: + isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ConditionalParent( + condition: !isDesktop, + builder: + (child) => Row( + children: [ + Expanded(child: child), + SimpleCopyButton( + data: + _transaction.txid, + ), + ], + ), + child: Text( + "Transaction ID", + 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< + StackColors + >()! + .textDark, ) - .hideBlockExplorerWarning == - false) { - final shouldContinue = - await showExplorerWarning( - "${uri.scheme}://${uri.host}", - ); + : STextStyles.itemSubtitle12( + context, + ), + ), + if (coin is! Epiccash) + const SizedBox(height: 8), + if (coin is! Epiccash) + CustomTextButton( + text: + "Open in block explorer", + onTap: () async { + final uri = + getBlockExplorerTransactionUrlFor( + coin: coin, + txid: + _transaction.txid, + ); + + if (ref + .read( + prefsChangeNotifierProvider, + ) + .hideBlockExplorerWarning == + false) { + final shouldContinue = + await showExplorerWarning( + "${uri.scheme}://${uri.host}", + ); - if (!shouldContinue) { - return; - } - } - - // ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = false; - try { - await launchUrl( - uri, - mode: - LaunchMode - .externalApplication, - ); - } catch (_) { - if (context.mounted) { - unawaited( - showDialog( - context: context, - builder: - ( - _, - ) => StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", - ), - ), - ); - } - } finally { - // Future.delayed( - // const Duration(seconds: 1), - // () => ref - // .read( - // shouldShowLockscreenOnResumeStateProvider - // .state) - // .state = true, - // ); - } - }, - ), - // ), - // ), - ], - ), + if (!shouldContinue) { + return; + } + } + + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + try { + await launchUrl( + uri, + mode: + LaunchMode + .externalApplication, + ); + } catch (_) { + if (context.mounted) { + unawaited( + showDialog( + context: context, + builder: + ( + _, + ) => StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), + ), + ); + } + } finally { + // Future.delayed( + // const Duration(seconds: 1), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + } + }, + ), + // ), + // ), + ], + ), + ), + if (isDesktop) + const SizedBox(width: 12), + if (isDesktop) + IconCopyButton( + data: _transaction.txid, + ), + ], ), - if (isDesktop) const SizedBox(width: 12), - if (isDesktop) - IconCopyButton(data: _transaction.txid), - ], - ), - ), + ), // if ((coin is FiroTestNet || coin is Firo) && // _transaction.subType == "mint") // const SizedBox( diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 06697a1d9..dd4c29560 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -56,6 +56,7 @@ import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../wallets/wallet/intermediate/lib_salvium_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/mweb_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/spark_interface.dart'; @@ -432,9 +433,9 @@ class _WalletViewState extends ConsumerState { ), ), ); - final firoWallet = ref.read(pWallets).getWallet(walletId) as FiroWallet; + final wallet = ref.read(pWallets).getWallet(walletId); - final Amount publicBalance = firoWallet.info.cachedBalance.spendable; + final Amount publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { @@ -444,7 +445,7 @@ class _WalletViewState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -453,7 +454,11 @@ class _WalletViewState extends ConsumerState { } try { - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { Navigator.of( @@ -462,7 +467,7 @@ class _WalletViewState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -477,7 +482,7 @@ class _WalletViewState extends ConsumerState { context: context, builder: (_) => StackOkDialog( - title: "Anonymize all failed", + title: "Privatize all failed", message: "Reason: $e", ), ); @@ -811,8 +816,11 @@ class _WalletViewState extends ConsumerState { ), ), ), - if (isSparkWallet) const SizedBox(height: 10), - if (isSparkWallet) + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) + const SizedBox(height: 10), + if (isSparkWallet || + ref.watch(pWalletInfo(walletId)).isMwebEnabled) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( @@ -831,7 +839,7 @@ class _WalletViewState extends ConsumerState { (context) => StackDialog( title: "Attention!", message: - "You're about to anonymize all of your public funds.", + "You're about to privatize all of your public funds.", leftButton: TextButton( onPressed: () { Navigator.of(context).pop(); @@ -872,7 +880,7 @@ class _WalletViewState extends ConsumerState { ); }, child: Text( - "Anonymize funds", + "Privatize funds", style: STextStyles.button( context, ).copyWith( diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 1133731dc..aad2dc5db 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -219,6 +219,11 @@ class _FavoriteCardState extends ConsumerState { ref.watch(pWalletBalanceSecondary(walletId)).total; total += ref.watch(pWalletBalanceTertiary(walletId)).total; + } else if (ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + )) { + total += + ref.watch(pWalletBalanceSecondary(walletId)).total; } Amount fiatTotal = Amount.zero; diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index c66fb387d..a5e0ba924 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -16,6 +16,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; import '../../db/isar/main_db.dart'; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../themes/coin_icon_provider.dart'; import '../../themes/stack_colors.dart'; @@ -41,6 +42,9 @@ import '../../widgets/toggle.dart'; import 'utxo_row.dart'; final desktopUseUTXOs = StateProvider((ref) => {}); +final pDesktopUseUTXOs = Provider( + (ref) => ref.watch(desktopUseUTXOs).map((e) => StandardInput(e)).toSet(), +); class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { const DesktopCoinControlUseDialog({ @@ -124,21 +128,22 @@ class _DesktopCoinControlUseDialogState ); } - final Amount selectedSum = _selectedUTXOs.map((e) => e.value).fold( - Amount( - rawValue: BigInt.zero, - fractionDigits: coin.fractionDigits, - ), - (value, element) => value += Amount( - rawValue: BigInt.from(element), - fractionDigits: coin.fractionDigits, - ), + final Amount selectedSum = _selectedUTXOs + .map((e) => e.value) + .fold( + Amount(rawValue: BigInt.zero, fractionDigits: coin.fractionDigits), + (value, element) => + value += Amount( + rawValue: BigInt.from(element), + fractionDigits: coin.fractionDigits, + ), ); - final enableApply = widget.amountToSend == null - ? selectedChanged(_selectedUTXOs) - : selectedChanged(_selectedUTXOs) && - widget.amountToSend! <= selectedSum; + final enableApply = + widget.amountToSend == null + ? selectedChanged(_selectedUTXOs) + : selectedChanged(_selectedUTXOs) && + widget.amountToSend! <= selectedSum; return DesktopDialog( maxWidth: 700, @@ -147,14 +152,8 @@ class _DesktopCoinControlUseDialogState children: [ Row( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, - ), - Text( - "Coin control", - style: STextStyles.desktopH3(context), - ), + const AppBarBackButton(size: 40, iconSize: 24), + Text("Coin control", style: STextStyles.desktopH3(context)), ], ), Expanded( @@ -164,24 +163,24 @@ class _DesktopCoinControlUseDialogState children: [ RoundedContainer( color: Colors.transparent, - borderColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, + borderColor: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "This option allows you to control, freeze, and utilize " "outputs at your discretion.", - style: - STextStyles.desktopTextExtraExtraSmall(context), + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ], ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( @@ -199,11 +198,13 @@ class _DesktopCoinControlUseDialogState _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( @@ -223,44 +224,47 @@ class _DesktopCoinControlUseDialogState 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, ), - ), - ) - : null, + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), SizedBox( height: 56, width: 240, child: Toggle( isOn: _filter == CCFilter.frozen, - onColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOn, - offColor: Theme.of(context) - .extension()! - .rateTypeToggleDesktopColorOff, + onColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn, + offColor: + Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff, onIcon: Assets.svg.coinControl.unBlocked, onText: "Available", offIcon: Assets.svg.coinControl.blocked, @@ -281,9 +285,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), JDropdownIconButton( redrawOnScreenSizeChanged: true, groupValue: _sort, @@ -299,164 +301,169 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Expanded( - child: _list != null - ? ListView.separated( - shrinkWrap: true, - primary: false, - itemCount: _list!.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(_list![index]) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = _selectedUTXOsData.contains(data); + child: + _list != null + ? ListView.separated( + shrinkWrap: true, + primary: false, + itemCount: _list!.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(_list![index]) + .findFirstSync()!; + final data = UtxoRowData(utxo.id, false); + data.selected = _selectedUTXOsData.contains( + data, + ); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove(value); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }, + ) + : ListView.separated( + itemCount: _map!.entries.length, + separatorBuilder: + (context, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final entry = _map!.entries.elementAt(index); + final _controller = RotateIconController(); + + return Expandable2( + border: + Theme.of(context) + .extension()! + .backgroundAppBar, + background: + Theme.of( + context, + ).extension()!.popupBG, + animationDurationMultiplier: + 0.2 * entry.value.length, + onExpandWillChange: (state) { + if (state == Expandable2State.expanded) { + _controller.forward?.call(); } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); + _controller.reverse?.call(); } - }); - }, - ); - }, - ) - : ListView.separated( - itemCount: _map!.entries.length, - separatorBuilder: (context, _) => const SizedBox( - height: 10, - ), - itemBuilder: (context, index) { - final entry = _map!.entries.elementAt(index); - final _controller = RotateIconController(); - - return Expandable2( - border: Theme.of(context) - .extension()! - .backgroundAppBar, - background: Theme.of(context) - .extension()! - .popupBG, - animationDurationMultiplier: - 0.2 * entry.value.length, - onExpandWillChange: (state) { - if (state == Expandable2State.expanded) { - _controller.forward?.call(); - } else { - _controller.reverse?.call(); - } - }, - header: RoundedContainer( - padding: const EdgeInsets.all(20), - color: Colors.transparent, - child: Row( - children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + }, + header: RoundedContainer( + padding: const EdgeInsets.all(20), + color: Colors.transparent, + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 24, + height: 24, ), - width: 24, - height: 24, - ), - const SizedBox( - width: 12, - ), - Expanded( - flex: 3, - child: Text( - entry.key, - style: STextStyles.w600_14(context), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Text( + entry.key, + style: STextStyles.w600_14(context), + ), ), - ), - Expanded( - child: Text( - "${entry.value.length} " - "output${entry.value.length > 1 ? "s" : ""}", - style: STextStyles - .desktopTextExtraExtraSmall( - context, + Expanded( + child: Text( + "${entry.value.length} " + "output${entry.value.length > 1 ? "s" : ""}", + style: + STextStyles.desktopTextExtraExtraSmall( + context, + ), ), ), - ), - RotateIcon( - animationDurationMultiplier: - 0.2 * entry.value.length, - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 14, - color: Theme.of(context) - .extension()! - .textSubtitle1, + RotateIcon( + animationDurationMultiplier: + 0.2 * entry.value.length, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + color: + Theme.of(context) + .extension()! + .textSubtitle1, + ), + curve: Curves.easeInOut, + controller: _controller, ), - curve: Curves.easeInOut, - controller: _controller, - ), - ], + ], + ), ), - ), - children: entry.value.map( - (id) { - final utxo = MainDB.instance.isar.utxos - .where() - .idEqualTo(id) - .findFirstSync()!; - final data = UtxoRowData(utxo.id, false); - data.selected = - _selectedUTXOsData.contains(data); + children: + entry.value.map((id) { + final utxo = + MainDB.instance.isar.utxos + .where() + .idEqualTo(id) + .findFirstSync()!; + final data = UtxoRowData( + utxo.id, + false, + ); + data.selected = _selectedUTXOsData + .contains(data); - return UtxoRow( - key: Key( - "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", - ), - data: data, - compact: true, - compactWithBorder: false, - raiseOnSelected: false, - walletId: widget.walletId, - onSelectionChanged: (value) { - setState(() { - if (data.selected) { - _selectedUTXOsData.add(value); - _selectedUTXOs.add(utxo); - } else { - _selectedUTXOsData.remove(value); - _selectedUTXOs.remove(utxo); - } - }); - }, - ); - }, - ).toList(), - ); - }, - ), - ), - const SizedBox( - height: 16, + return UtxoRow( + key: Key( + "${utxo.walletId}_${utxo.id}_${utxo.isBlocked}", + ), + data: data, + compact: true, + compactWithBorder: false, + raiseOnSelected: false, + walletId: widget.walletId, + onSelectionChanged: (value) { + setState(() { + if (data.selected) { + _selectedUTXOsData.add(value); + _selectedUTXOs.add(utxo); + } else { + _selectedUTXOsData.remove( + value, + ); + _selectedUTXOs.remove(utxo); + } + }); + }, + ); + }).toList(), + ); + }, + ), ), + const SizedBox(height: 16), RoundedContainer( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, padding: EdgeInsets.zero, child: ConditionalParent( condition: widget.amountToSend != null, @@ -467,9 +474,10 @@ class _DesktopCoinControlUseDialogState child, Container( height: 1.2, - color: Theme.of(context) - .extension()! - .popupBG, + color: + Theme.of( + context, + ).extension()!.popupBG, ), Padding( padding: const EdgeInsets.all(16), @@ -481,26 +489,26 @@ class _DesktopCoinControlUseDialogState "Amount to send", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), SelectableText( - "${widget.amountToSend!.decimal.toStringAsFixed( - coin.fractionDigits, - )}" + "${widget.amountToSend!.decimal.toStringAsFixed(coin.fractionDigits)}" " ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), + context, + ).copyWith( + color: + Theme.of(context) + .extension()! + .textDark, + ), ), ], ), @@ -518,23 +526,23 @@ class _DesktopCoinControlUseDialogState style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), SelectableText( - "${selectedSum.decimal.toStringAsFixed( - coin.fractionDigits, - )} ${coin.ticker}", + "${selectedSum.decimal.toStringAsFixed(coin.fractionDigits)} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: widget.amountToSend == null - ? Theme.of(context) - .extension()! - .textDark - : selectedSum < widget.amountToSend! + color: + widget.amountToSend == null + ? Theme.of( + context, + ).extension()!.textDark + : selectedSum < widget.amountToSend! ? Theme.of(context) .extension()! .accentColorRed @@ -548,18 +556,17 @@ class _DesktopCoinControlUseDialogState ), ), ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), Row( children: [ Expanded( child: SecondaryButton( enabled: _selectedUTXOsData.isNotEmpty, buttonHeight: ButtonHeight.l, - label: _selectedUTXOsData.isEmpty - ? "Clear selection" - : "Clear selection (${_selectedUTXOsData.length})", + label: + _selectedUTXOsData.isEmpty + ? "Clear selection" + : "Clear selection (${_selectedUTXOsData.length})", onPressed: () { setState(() { _selectedUTXOsData.clear(); @@ -568,9 +575,7 @@ class _DesktopCoinControlUseDialogState }, ), ), - const SizedBox( - width: 20, - ), + const SizedBox(width: 20), Expanded( child: PrimaryButton( enabled: enableApply, @@ -586,9 +591,7 @@ class _DesktopCoinControlUseDialogState ), ], ), - const SizedBox( - height: 16, - ), + const SizedBox(height: 16), ], ), ), diff --git a/lib/pages_desktop_specific/mweb_utxos_view.dart b/lib/pages_desktop_specific/mweb_utxos_view.dart new file mode 100644 index 000000000..0c28cdf66 --- /dev/null +++ b/lib/pages_desktop_specific/mweb_utxos_view.dart @@ -0,0 +1,281 @@ +/* + * 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 2025-06-12 + * + */ + +import 'dart:async'; + +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/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/background.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 '../db/drift/database.dart'; +import '../providers/providers.dart'; +import '../widgets/detail_item.dart'; +import '../widgets/rounded_white_container.dart'; + +class MwebUtxosView extends ConsumerWidget { + const MwebUtxosView({super.key, required this.walletId}); + + static const title = "MWEB outputs"; + static const String routeName = "/mwebUtxosView"; + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: 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(title, style: STextStyles.desktopH3(context)), + const Spacer(), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding(padding: const EdgeInsets.all(24), child: child), + ); + }, + child: ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: AppBarBackButton( + onPressed: () => Navigator.of(context).pop(), + ), + title: Text(title, style: STextStyles.navBarTitle(context)), + ), + body: SafeArea(child: child), + ), + ); + }, + child: StreamViewList( + itemName: title, + stream: + ref + .read(pDrift(walletId)) + .select(ref.read(pDrift(walletId)).mwebUtxos) + .watch(), + itemBuilder: (MwebUtxo? coin) { + return [ + ("Output Id", coin?.outputId ?? "", 9), + ("Address", coin?.address ?? "", 9), + ("Value (sats)", coin?.value.toString() ?? "", 3), + ("Height", coin?.height.toString() ?? "", 2), + ("Block time", coin?.blockTime.toString() ?? "", 2), + ("Blocked", coin?.blocked.toString() ?? "", 2), + ("Used", coin?.used.toString() ?? "", 2), + ]; + }, + ), + ), + ); + } +} + +class StreamViewList extends StatefulWidget { + const StreamViewList({ + super.key, + required this.stream, + required this.itemBuilder, + required this.itemName, + }); + + final Stream> stream; + final String itemName; + final List<(String title, String value, int flex)> Function(T?) itemBuilder; + + @override + State> createState() => _StreamViewListState(); +} + +class _StreamViewListState extends State> { + List _items = []; + + late final StreamSubscription> _streamSubscription; + + void _onMwebUtxossCollectionWatcherEvent(List items) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _items = items; + }); + } + }); + } + + @override + void initState() { + super.initState(); + + _streamSubscription = widget.stream.listen( + (data) => _onMwebUtxossCollectionWatcherEvent(data), + ); + } + + @override + void dispose() { + _streamSubscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + children: [ + ...widget + .itemBuilder(null) + .map( + (e) => Expanded( + flex: e.$3, + child: Text( + e.$1, + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: ListView.separated( + shrinkWrap: true, + itemCount: _items.length, + separatorBuilder: + (_, __) => Container( + height: 1, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, + ), + itemBuilder: + (_, index) => Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ...widget + .itemBuilder(_items[index]) + .map( + (e) => Expanded( + flex: e.$3, + child: SelectableText( + e.$2, + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.left, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } else { + return ListView.builder( + itemCount: _items.length + 1, + itemBuilder: (ctx, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16), + child: RoundedWhiteContainer( + child: + index == 0 + ? Row( + children: [ + Text( + "Total ${widget.itemName}: ${_items.length}", + style: STextStyles.itemSubtitle(context), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...widget + .itemBuilder(_items[index - 1]) + .map( + (e) => DetailItem(title: e.$1, detail: e.$2), + ), + ], + ), + ), + ); + }, + ); + } + } +} 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 e35e6ed34..96363f1e2 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 @@ -28,7 +28,6 @@ import '../../../pages/wallet_view/sub_widgets/transactions_list.dart'; import '../../../pages/wallet_view/transaction_views/all_transactions_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/all_transactions_v2_view.dart'; import '../../../pages/wallet_view/transaction_views/tx_v2/transaction_v2_list.dart'; -import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/active_wallet_provider.dart'; import '../../../providers/global/auto_swb_service_provider.dart'; import '../../../providers/providers.dart'; @@ -46,6 +45,7 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/banano_wallet.dart'; import '../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../wallets/wallet/wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/custom_buttons/blue_text_button.dart'; @@ -57,6 +57,7 @@ import '../../coin_control/desktop_coin_control_use_dialog.dart'; import 'sub_widgets/desktop_wallet_features.dart'; import 'sub_widgets/desktop_wallet_summary.dart'; import 'sub_widgets/firo_desktop_wallet_summary.dart'; +import 'sub_widgets/mweb_desktop_wallet_summary.dart'; import 'sub_widgets/my_wallet.dart'; import 'sub_widgets/network_info_button.dart'; import 'sub_widgets/wallet_keys_button.dart'; @@ -448,13 +449,26 @@ class DesktopWalletHeaderRow extends ConsumerWidget { ), if (wallet is! FiroWallet) - DesktopWalletSummary( - walletId: wallet.walletId, - initialSyncStatus: - wallet.refreshMutex.isLocked - ? WalletSyncStatus.syncing - : WalletSyncStatus.synced, - ), + wallet is MwebInterface && + ref.watch( + pWalletInfo( + wallet.walletId, + ).select((s) => s.isMwebEnabled), + ) + ? MwebDesktopWalletSummary( + walletId: wallet.walletId, + initialSyncStatus: + wallet.refreshMutex.isLocked + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ) + : 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_balance_toggle_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart index 73a6bd742..f2c5429db 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart @@ -89,12 +89,12 @@ class DesktopPrivateBalanceToggleButton extends ConsumerWidget { color: Theme.of(context).extension()!.buttonBackSecondary, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - if (currentType != FiroType.spark) { + if (currentType != BalanceType.private) { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.spark; + BalanceType.private; } else { ref.read(publicPrivateBalanceStateProvider.state).state = - FiroType.public; + BalanceType.public; } onPressed?.call(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index b90cc2f6a..5cea18ff2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -22,7 +22,6 @@ import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/receive_view/generate_receiving_uri_qr_code_view.dart'; -import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -31,6 +30,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../../utilities/show_loading.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/crypto_currency/crypto_currency.dart'; @@ -41,6 +41,7 @@ import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/view_only_option_interface.dart'; import '../../../../widgets/conditional_parent.dart'; @@ -72,6 +73,7 @@ class _DesktopReceiveState extends ConsumerState { late final String walletId; late final ClipboardInterface clipboard; late final bool supportsSpark; + late bool supportsMweb; late final bool showMultiType; int _currentIndex = 0; @@ -91,10 +93,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -109,8 +110,9 @@ class _DesktopReceiveState extends ConsumerState { if (wallet is Bip39HDWallet && wallet is! BCashInterface) { DerivePathType? type; if (wallet.isViewOnly && wallet is ExtendedKeysInterface) { - final voData = await wallet.getViewOnlyWalletData() - as ExtendedKeysViewOnlyWalletData; + final voData = + await wallet.getViewOnlyWalletData() + as ExtendedKeysViewOnlyWalletData; for (final t in wallet.cryptoCurrency.supportedDerivationPathTypes) { final testPath = wallet.cryptoCurrency.constructDerivePath( derivePathType: t, @@ -168,10 +170,9 @@ class _DesktopReceiveState extends ConsumerState { return WillPopScope( onWillPop: () async => shouldPop, child: Container( - color: Theme.of(context) - .extension()! - .overlay - .withOpacity(0.5), + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), child: const CustomLoadingOverlay( message: "Generating address", eventBus: null, @@ -198,18 +199,50 @@ class _DesktopReceiveState extends ConsumerState { } } + Future
_generateNewMwebAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId) as MwebInterface; + + final address = await wallet.generateNextMwebAddress(); + await ref.read(mainDBProvider).isar.writeTxn(() async { + await ref.read(mainDBProvider).isar.addresses.put(address); + }); + + return address; + } + + Future generateNewMwebAddress() async { + final address = await showLoading
( + whileFuture: _generateNewMwebAddress(), + context: context, + message: "Generating address", + rootNavigator: Util.isDesktop, + ); + + if (mounted && address != null) { + setState(() { + _addressMap[AddressType.mweb] = address.value; + }); + } + } + @override void initState() { walletId = widget.walletId; coin = ref.read(pWalletInfo(walletId)).coin; clipboard = widget.clipboard; final wallet = ref.read(pWallets).getWallet(walletId); - supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; + supportsSpark = wallet is SparkInterface; + supportsMweb = + wallet is MwebInterface && + !wallet.info.isViewOnly && + wallet.info.isMwebEnabled; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly) { showMultiType = false; } else { - showMultiType = supportsSpark || + showMultiType = + supportsSpark || + supportsMweb || (wallet is! BCashInterface && wallet is Bip39HDWallet && wallet.supportedAddressTypes.length > 1); @@ -222,10 +255,14 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.insert(0, AddressType.spark); } else { _walletAddressTypes.addAll( - (wallet as Bip39HDWallet) - .supportedAddressTypes - .where((e) => e != wallet.info.mainAddressType), + (wallet as Bip39HDWallet).supportedAddressTypes.where( + (e) => e != wallet.info.mainAddressType, + ), ); + + if (supportsMweb) { + _walletAddressTypes.insert(0, AddressType.mweb); + } } } @@ -233,8 +270,9 @@ class _DesktopReceiveState extends ConsumerState { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); } - _addressMap[_walletAddressTypes[_currentIndex]] = - ref.read(pWalletReceivingAddress(walletId)); + _addressMap[_walletAddressTypes[_currentIndex]] = ref.read( + pWalletReceivingAddress(walletId), + ); if (showMultiType) { for (final type in _walletAddressTypes) { @@ -246,19 +284,22 @@ class _DesktopReceiveState extends ConsumerState { .walletIdEqualTo(walletId) .filter() .typeEqualTo(type) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) .sortByDerivationIndexDesc() .findFirst() .asStream() .listen((event) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _addressMap[type] = - event?.value ?? _addressMap[type] ?? "[No address yet]"; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[type] = + event?.value ?? _addressMap[type] ?? "[No address yet]"; + }); + } }); - } - }); - }); + }); } } @@ -277,6 +318,57 @@ class _DesktopReceiveState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + ref.listen(pWalletInfo(walletId), (prev, next) { + if (prev?.isMwebEnabled != next.isMwebEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + supportsMweb = next.isMwebEnabled; + + if (supportsMweb && + !_walletAddressTypes.contains(AddressType.mweb)) { + _walletAddressTypes.insert(0, AddressType.mweb); + _addressSubMap[AddressType.mweb] = ref + .read(mainDBProvider) + .isar + .addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .not() + .subTypeEqualTo(AddressSubType.change) + .sortByDerivationIndexDesc() + .findFirst() + .asStream() + .listen((event) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _addressMap[AddressType.mweb] = + event?.value ?? + _addressMap[AddressType.mweb] ?? + "[No address yet]"; + }); + } + }); + }); + } else { + _walletAddressTypes.remove(AddressType.mweb); + _addressSubMap[AddressType.mweb]?.cancel(); + _addressSubMap.remove(AddressType.mweb); + } + + if (_currentIndex >= _walletAddressTypes.length) { + _currentIndex = _walletAddressTypes.length - 1; + } + }); + } + }); + } + }); + final String address; if (showMultiType) { address = _addressMap[_walletAddressTypes[_currentIndex]]!; @@ -284,8 +376,9 @@ class _DesktopReceiveState extends ConsumerState { address = ref.watch(pWalletReceivingAddress(walletId)); } - final wallet = - ref.watch(pWallets.select((value) => value.getWallet(walletId))); + final wallet = ref.watch( + pWallets.select((value) => value.getWallet(walletId)), + ); final bool canGen; if (wallet is ViewOnlyOptionInterface && @@ -293,7 +386,8 @@ class _DesktopReceiveState extends ConsumerState { wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { canGen = false; } else { - canGen = (wallet is MultiAddressInterface || supportsSpark); + canGen = + (wallet is MultiAddressInterface || supportsSpark || supportsMweb); } return Column( @@ -301,91 +395,90 @@ class _DesktopReceiveState extends ConsumerState { children: [ ConditionalParent( condition: showMultiType, - builder: (child) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - DropdownButtonHideUnderline( - child: DropdownButton2( - value: _currentIndex, - items: [ - for (int i = 0; i < _walletAddressTypes.length; i++) - DropdownMenuItem( - value: i, - child: Text( - supportsSpark && - _walletAddressTypes[i] == AddressType.p2pkh - ? "Transparent address" - : "${_walletAddressTypes[i].readableName} address", - style: STextStyles.w500_14(context), + builder: + (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DropdownButtonHideUnderline( + child: DropdownButton2( + value: _currentIndex, + items: [ + for (int i = 0; i < _walletAddressTypes.length; i++) + DropdownMenuItem( + value: i, + child: Text( + supportsSpark && + _walletAddressTypes[i] == + AddressType.p2pkh + ? "Transparent address" + : "${_walletAddressTypes[i].readableName} address", + style: STextStyles.w500_14(context), + ), + ), + ], + onChanged: (value) { + if (value != null && value != _currentIndex) { + setState(() { + _currentIndex = value; + }); + } + }, + isExpanded: true, + 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, + ), ), ), - ], - onChanged: (value) { - if (value != null && value != _currentIndex) { - setState(() { - _currentIndex = value; - }); - } - }, - isExpanded: true, - 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, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - buttonStyleData: ButtonStyleData( - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), ), - ), - ), - 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, + ), ), ), ), - menuItemStyleData: const MenuItemStyleData( - padding: EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - ), + const SizedBox(height: 12), + child, + ], ), - const SizedBox( - height: 12, - ), - child, - ], - ), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () { - clipboard.setData( - ClipboardData( - text: address, - ), - ); + clipboard.setData(ClipboardData(text: address)); showFloatingFlushBar( type: FlushBarType.info, message: "Copied to clipboard", @@ -396,9 +489,10 @@ class _DesktopReceiveState extends ConsumerState { child: Container( decoration: BoxDecoration( border: Border.all( - color: Theme.of(context) - .extension()! - .backgroundAppBar, + color: + Theme.of( + context, + ).extension()!.backgroundAppBar, width: 1, ), borderRadius: BorderRadius.circular( @@ -411,11 +505,7 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Text( - "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( - pCurrentTokenWallet.select( - (value) => value!.tokenContract.symbol, - ), - )} address", + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch(pCurrentTokenWallet.select((value) => value!.tokenContract.symbol))} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -425,24 +515,18 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.copy, width: 15, height: 15, - color: Theme.of(context) - .extension()! - .infoItemIcons, - ), - const SizedBox( - width: 4, - ), - Text( - "Copy", - style: STextStyles.link2(context), + color: + Theme.of( + context, + ).extension()!.infoItemIcons, ), + const SizedBox(width: 4), + Text("Copy", style: STextStyles.link2(context)), ], ), ], ), - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ Expanded( @@ -451,9 +535,10 @@ class _DesktopReceiveState extends ConsumerState { style: STextStyles.desktopTextExtraExtraSmall( context, ).copyWith( - color: Theme.of(context) - .extension()! - .textDark, + color: + Theme.of( + context, + ).extension()!.textDark, ), ), ), @@ -467,85 +552,78 @@ class _DesktopReceiveState extends ConsumerState { ), ), - if (canGen) - const SizedBox( - height: 20, - ), + if (canGen) const SizedBox(height: 20), if (canGen) SecondaryButton( buttonHeight: ButtonHeight.l, - onPressed: supportsSpark && - _walletAddressTypes[_currentIndex] == AddressType.spark - ? generateNewSparkAddress - : generateNewAddress, + onPressed: + supportsMweb && + _walletAddressTypes[_currentIndex] == AddressType.mweb + ? generateNewMwebAddress + : supportsSpark && + _walletAddressTypes[_currentIndex] == AddressType.spark + ? generateNewSparkAddress + : generateNewAddress, label: "Generate new address", ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), Center( child: QR( - data: AddressUtils.buildUriString( - coin.uriScheme, - address, - {}, - ), + data: AddressUtils.buildUriString(coin.uriScheme, address, {}), size: 200, ), ), - const SizedBox( - height: 32, - ), + const SizedBox(height: 32), // TODO: create transparent button class to account for hover GestureDetector( onTap: () async { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( + builder: + (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( children: [ - const AppBarBackButton( - size: 40, - iconSize: 24, + Row( + children: [ + const AppBarBackButton(size: 40, iconSize: 24), + Text( + "Generate QR code", + style: STextStyles.desktopH3(context), + ), + ], ), - Text( - "Generate QR code", - style: STextStyles.desktopH3(context), + IntrinsicHeight( + child: Navigator( + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: + (_, __) => [ + RouteGenerator.generateRoute( + RouteSettings( + name: GenerateUriQrCodeView.routeName, + arguments: Tuple2(coin, address), + ), + ), + ], + ), ), ], ), - IntrinsicHeight( - child: Navigator( - onGenerateRoute: RouteGenerator.generateRoute, - onGenerateInitialRoutes: (_, __) => [ - RouteGenerator.generateRoute( - RouteSettings( - name: GenerateUriQrCodeView.routeName, - arguments: Tuple2(coin, address), - ), - ), - ], - ), - ), - ], - ), - ), + ), ); } else { unawaited( Navigator.of(context).push( RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: coin, - receivingAddress: address, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: coin, + receivingAddress: address, + ), settings: const RouteSettings( name: GenerateUriQrCodeView.routeName, ), @@ -564,21 +642,21 @@ class _DesktopReceiveState extends ConsumerState { Assets.svg.qrcode, width: 14, height: 16, - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - const SizedBox( - width: 8, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), + const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( "Create new QR code", style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, + color: + Theme.of( + context, + ).extension()!.accentColorBlue, ), ), ), 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 206e7ce6d..567f3a94a 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 @@ -17,6 +17,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../../../models/isar/models/blockchain_data/address.dart'; import '../../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../../models/isar/models/contact_entry.dart'; import '../../../../models/paynym/paynym_account_lite.dart'; @@ -49,6 +50,7 @@ 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/mweb_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; @@ -163,13 +165,16 @@ class _DesktopSendState extends ConsumerState { final Amount amount = ref.read(pSendAmount)!; final Amount availableBalance; - if ((coin is Firo)) { + if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: availableBalance = wallet.info.cachedBalance.spendable; break; - case FiroType.spark: - availableBalance = wallet.info.cachedBalanceTertiary.spendable; + case BalanceType.private: + availableBalance = + coin is Firo + ? wallet.info.cachedBalanceTertiary.spendable + : wallet.info.cachedBalanceSecondary.spendable; break; } } else { @@ -280,7 +285,7 @@ class _DesktopSendState extends ConsumerState { ref .read(publicPrivateBalanceStateProvider.state) .state == - FiroType.spark, + BalanceType.private, onCancel: () { wasCancelled = true; @@ -307,10 +312,11 @@ class _DesktopSendState extends ConsumerState { txData: TxData( paynymAccountLite: widget.accountLite!, recipients: [ - ( + TxRecipient( address: widget.accountLite!.code, amount: amount, isChange: false, + addressType: AddressType.unknown, ), ], satsPerVByte: isCustomFee ? customFeeRate : null, @@ -318,14 +324,14 @@ class _DesktopSendState extends ConsumerState { utxos: (wallet is CoinControlInterface && coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); } else if (wallet is FiroWallet) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: if (ref.read(pValidSparkSendToAddress)) { txDataFuture = wallet.prepareSparkMintTransaction( txData: TxData( @@ -341,8 +347,8 @@ class _DesktopSendState extends ConsumerState { satsPerVByte: isCustomFee ? customFeeRate : null, utxos: (coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); @@ -350,28 +356,42 @@ class _DesktopSendState extends ConsumerState { txDataFuture = wallet.prepareSend( txData: TxData( recipients: [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType(_address!)!, + ), ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, utxos: (coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ), ); } break; - case FiroType.spark: + case BalanceType.private: txDataFuture = wallet.prepareSendSpark( txData: TxData( recipients: ref.read(pValidSparkSendToAddress) ? null : [ - (address: _address!, amount: amount, isChange: false), + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + wallet.cryptoCurrency.getAddressType( + _address!, + )!, + ), ], sparkRecipients: ref.read(pValidSparkSendToAddress) @@ -388,11 +408,41 @@ class _DesktopSendState extends ConsumerState { ); break; } + } else if (wallet is MwebInterface && + ref.read(publicPrivateBalanceStateProvider) == BalanceType.private) { + txDataFuture = wallet.prepareSendMweb( + txData: TxData( + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], + feeRateType: ref.read(feeRateTypeDesktopStateProvider), + satsPerVByte: isCustomFee ? customFeeRate : null, + // these will need to be mweb utxos + // utxos: + // (wallet is CoinControlInterface && + // coinControlEnabled && + // ref.read(pDesktopUseUTXOs).isNotEmpty) + // ? ref.read(pDesktopUseUTXOs) + // : null, + ), + ); } else { final memo = isStellar ? memoController.text : null; txDataFuture = wallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: wallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], memo: memo, feeRateType: ref.read(feeRateTypeDesktopStateProvider), satsPerVByte: isCustomFee ? customFeeRate : null, @@ -403,8 +453,8 @@ class _DesktopSendState extends ConsumerState { utxos: (wallet is CoinControlInterface && coinControlEnabled && - ref.read(desktopUseUTXOs).isNotEmpty) - ? ref.read(desktopUseUTXOs) + ref.read(pDesktopUseUTXOs).isNotEmpty) + ? ref.read(pDesktopUseUTXOs) : null, ethEIP1559Fee: ethFee, ), @@ -624,7 +674,8 @@ class _DesktopSendState extends ConsumerState { ref.read(pIsExchangeAddress.state).state = (coin as Firo) .isExchangeAddress(address ?? ""); - if (ref.read(publicPrivateBalanceStateProvider) == FiroType.spark && + if (ref.read(publicPrivateBalanceStateProvider) == + BalanceType.private && ref.read(pIsExchangeAddress) && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -831,13 +882,16 @@ class _DesktopSendState extends ConsumerState { if (showCoinControl && ref.read(desktopUseUTXOs).isNotEmpty) { amount = _selectedUtxosAmount(ref.read(desktopUseUTXOs)); - } else if (coin is Firo) { + } else if (coin is Firo || ref.read(pWalletInfo(walletId)).isMwebEnabled) { switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.public: + case BalanceType.public: amount = ref.read(pWalletBalance(walletId)).spendable; break; - case FiroType.spark: - amount = ref.read(pWalletBalanceTertiary(walletId)).spendable; + case BalanceType.private: + amount = + coin is Firo + ? ref.read(pWalletBalanceTertiary(walletId)).spendable + : ref.read(pWalletBalanceSecondary(walletId)).spendable; break; } } else { @@ -969,12 +1023,16 @@ class _DesktopSendState extends ConsumerState { }); } - final firoType = ref.watch(publicPrivateBalanceStateProvider); + final balType = ref.watch(publicPrivateBalanceStateProvider); + final isMwebEnabled = ref.watch( + pWalletInfo(walletId).select((s) => s.isMwebEnabled), + ); + final showPrivateBalance = coin is Firo || isMwebEnabled; final isExchangeAddress = ref.watch(pIsExchangeAddress); ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (previous != next && - next == FiroType.spark && + next == BalanceType.private && isExchangeAddress && !_isFiroExWarningDisplayed) { _isFiroExWarningDisplayed = true; @@ -994,13 +1052,13 @@ class _DesktopSendState extends ConsumerState { ), ) && ref.watch(pWallets).getWallet(walletId) is CoinControlInterface && - (coin is Firo ? firoType == FiroType.public : true); + balType == BalanceType.public; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), - if (coin is Firo) + if (showPrivateBalance) Text( "Send from", style: STextStyles.desktopTextExtraSmall(context).copyWith( @@ -1011,19 +1069,19 @@ class _DesktopSendState extends ConsumerState { ), textAlign: TextAlign.left, ), - if (coin is Firo) const SizedBox(height: 10), - if (coin is Firo) + if (showPrivateBalance) const SizedBox(height: 10), + if (showPrivateBalance) DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, - value: firoType, + value: balType, items: [ DropdownMenuItem( - value: FiroType.spark, + value: BalanceType.private, child: Row( children: [ Text( - "Spark balance", + "Private balance", style: STextStyles.itemSubtitle12(context), ), const SizedBox(width: 10), @@ -1032,7 +1090,11 @@ class _DesktopSendState extends ConsumerState { .watch(pAmountFormatter(coin)) .format( ref - .watch(pWalletBalanceTertiary(walletId)) + .watch( + isMwebEnabled + ? pWalletBalanceSecondary(walletId) + : pWalletBalanceTertiary(walletId), + ) .spendable, ), style: STextStyles.itemSubtitle(context), @@ -1041,7 +1103,7 @@ class _DesktopSendState extends ConsumerState { ), ), DropdownMenuItem( - value: FiroType.public, + value: BalanceType.public, child: Row( children: [ Text( @@ -1062,8 +1124,8 @@ class _DesktopSendState extends ConsumerState { ), ], onChanged: (value) { - if (value is FiroType) { - if (value != FiroType.public) { + if (value is BalanceType) { + if (value != BalanceType.public) { ref.read(desktopUseUTXOs.state).state = {}; } setState(() { @@ -1098,7 +1160,7 @@ class _DesktopSendState extends ConsumerState { ), ), ), - if (coin is Firo) const SizedBox(height: 20), + if (showPrivateBalance) const SizedBox(height: 20), if (isPaynymSend) Text( "Send to PayNym address", 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 index 1eb046484..6b08923ab 100644 --- 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 @@ -71,7 +71,7 @@ class _DesktopSendFeeFormState extends ConsumerState { (cryptoCurrency is ElectrumXCurrencyInterface && !(((cryptoCurrency is Firo) && (ref.watch(publicPrivateBalanceStateProvider.state).state == - FiroType.spark)))); + BalanceType.private)))); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -183,7 +183,7 @@ class _DesktopSendFeeFormState extends ConsumerState { .state, ) .state != - FiroType.public) { + BalanceType.public) { final firoWallet = wallet as FiroWallet; if (ref @@ -192,7 +192,7 @@ class _DesktopSendFeeFormState extends ConsumerState { .state, ) .state == - FiroType.spark) { + BalanceType.private) { ref .read(feeSheetSessionCacheProvider) .average[amount] = await firoWallet 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 34071ea8e..bf57331ea 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 @@ -43,6 +43,7 @@ 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/primary_button.dart'; +import '../../../../widgets/desktop/qr_code_scanner_dialog.dart'; import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/eth_fee_form.dart'; import '../../../../widgets/icon_widgets/addressbook_icon.dart'; @@ -231,7 +232,15 @@ class _DesktopTokenSendState extends ConsumerState { txDataFuture = tokenWallet.prepareSend( txData: TxData( - recipients: [(address: _address!, amount: amount, isChange: false)], + recipients: [ + TxRecipient( + address: _address!, + amount: amount, + isChange: false, + addressType: + tokenWallet.cryptoCurrency.getAddressType(_address!)!, + ), + ], feeRateType: ref.read(feeRateTypeDesktopStateProvider), nonce: int.tryParse(nonceController.text), ethEIP1559Fee: ethFee, @@ -420,12 +429,20 @@ class _DesktopTokenSendState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await showDialog( + context: context, + builder: (context) => const QrCodeScannerDialog(), + ); + + if (qrResult == null) { + Logging.instance.w("Qr scanning cancelled"); + return; + } - Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + Logging.instance.d("qrResult content: $qrResult"); final paymentData = AddressUtils.parsePaymentUri( - qrResult.rawContent, + qrResult, logging: Logging.instance, ); @@ -464,7 +481,7 @@ class _DesktopTokenSendState extends ConsumerState { // now check for non standard encoded basic address } else { - _address = qrResult.rawContent.split("\n").first.trim(); + _address = qrResult.split("\n").first.trim(); sendToController.text = _address ?? ""; _updatePreviewButtonState(_address, _amountToSend); 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 2456a909c..63722608b 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 @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -44,6 +45,7 @@ import '../../../../wallets/wallet/intermediate/lib_salvium_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/mweb_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'; @@ -59,13 +61,14 @@ import '../../../cashfusion/desktop_cashfusion_view.dart'; import '../../../churning/desktop_churning_view.dart'; import '../../../coin_control/desktop_coin_control_view.dart'; import '../../../desktop_menu.dart'; +import '../../../mweb_utxos_view.dart'; import '../../../ordinals/desktop_ordinals_view.dart'; 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"), + anonymizeFunds("Privatize funds", "Privatize funds"), swap("Swap", ""), buy("Buy", "Buy cryptocurrency"), paynym("PayNym", "Increased address privacy using BIP47"), @@ -74,6 +77,7 @@ enum WalletFeature { "Control, freeze, and utilize outputs at your discretion", ), sparkCoins("Spark coins", "View wallet spark coins"), + mwebUtxos("MWEB outputs", "View wallet MWEB outputs"), ordinals("Ordinals", "View and control your ordinals in ${AppConfig.prefix}"), monkey("MonKey", "Generate Banano MonKey"), fusion("Fusion", "Decentralized mixing protocol"), @@ -84,7 +88,8 @@ enum WalletFeature { // special cases clearSparkCache("", ""), rbf("", ""), - reuseAddress("", ""); + reuseAddress("", ""), + enableMweb("", ""); final String label; final String description; @@ -138,6 +143,12 @@ class _DesktopWalletFeaturesState extends ConsumerState { ).pushNamed(SparkCoinsView.routeName, arguments: widget.walletId); } + void _onMwebUtxosPressed() { + Navigator.of( + context, + ).pushNamed(MwebUtxosView.routeName, arguments: widget.walletId); + } + Future _onAnonymizeAllPressed() async { await showDialog( context: context, @@ -153,7 +164,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { Text("Attention!", style: STextStyles.desktopH2(context)), const SizedBox(height: 16), Text( - "You're about to anonymize all of your public funds.", + "You're about to privatize all of your public funds.", style: STextStyles.desktopTextSmall(context), ), const SizedBox(height: 32), @@ -196,17 +207,16 @@ class _DesktopWalletFeaturesState extends ConsumerState { builder: (context) => WillPopScope( child: const CustomLoadingOverlay( - message: "Anonymizing balance", + message: "Privatizing balance", eventBus: null, ), onWillPop: () async => shouldPop, ), ), ); - final firoWallet = - ref.read(pWallets).getWallet(widget.walletId) as FiroWallet; - final publicBalance = firoWallet.info.cachedBalance.spendable; + final wallet = ref.read(pWallets).getWallet(widget.walletId); + final publicBalance = wallet.info.cachedBalance.spendable; if (publicBalance <= Amount.zero) { shouldPop = true; if (context.mounted) { @@ -217,7 +227,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.info, - message: "No funds available to anonymize!", + message: "No funds available to privatize!", context: context, ), ); @@ -226,7 +236,11 @@ class _DesktopWalletFeaturesState extends ConsumerState { } try { - await firoWallet.anonymizeAllSpark(); + if (wallet is MwebInterface && wallet.info.isMwebEnabled) { + await wallet.anonymizeAllMweb(); + } else { + await (wallet as FiroWallet).anonymizeAllSpark(); + } shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: true).pop(); @@ -236,7 +250,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { unawaited( showFloatingFlushBar( type: FlushBarType.success, - message: "Anonymize transaction submitted", + message: "Privatize transaction submitted", context: context, ), ); @@ -260,7 +274,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Anonymize all failed", + "Privatize all failed", style: STextStyles.desktopH3(context), ), const Spacer(flex: 1), @@ -381,7 +395,9 @@ class _DesktopWalletFeaturesState extends ConsumerState { final isViewOnly = wallet is ViewOnlyOptionInterface && wallet.isViewOnly; return [ - if (!isViewOnly && coin is Firo) + if (!isViewOnly && + (coin is Firo || + (wallet is MwebInterface && wallet.info.isMwebEnabled))) ( WalletFeature.anonymizeFunds, Assets.svg.recycle, @@ -413,6 +429,13 @@ class _DesktopWalletFeaturesState extends ConsumerState { _onSparkCoinsPressed, ), + if (kDebugMode && !isViewOnly && wallet is MwebInterface) + ( + WalletFeature.mwebUtxos, + Assets.svg.coinControl.gamePad, + _onMwebUtxosPressed, + ), + if (!isViewOnly && wallet is PaynymInterface) (WalletFeature.paynym, Assets.svg.robotHead, _onPaynymPressed), @@ -461,6 +484,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { wallet.isViewOnly && wallet.viewOnlyType == ViewOnlyWalletType.addressOnly; + final showMwebOption = wallet is MwebInterface && !wallet.isViewOnly; + final extraOptions = [ if (wallet is SparkInterface && !isViewOnly) (WalletFeature.clearSparkCache, Assets.svg.key, () => ()), @@ -469,6 +494,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (!isViewOnlyNoAddressGen) (WalletFeature.reuseAddress, Assets.svg.key, () => ()), + + if (showMwebOption) (WalletFeature.enableMweb, Assets.svg.key, () => ()), ]; return StaticOverflowRow( 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 84c52c870..c6ad2694d 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 @@ -104,12 +104,12 @@ class _WDesktopWalletSummaryState extends ConsumerState { final Amount balanceToShow; if (isFiro) { switch (ref.watch(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: final balance = ref.watch(pWalletBalanceTertiary(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; - case FiroType.public: + case BalanceType.public: final balance = ref.watch(pWalletBalance(walletId)); balanceToShow = _showAvailable ? balance.spendable : balance.total; break; 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 2de6daf39..650f67f68 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 @@ -104,7 +104,7 @@ class _WFiroDesktopWalletSummaryState children: [ TableRow( children: [ - const _Prefix(type: FiroType.spark), + const _Prefix(type: BalanceType.private), _Balance(coin: coin, amount: balanceToShowSpark), if (price != null) _Price( @@ -117,7 +117,7 @@ class _WFiroDesktopWalletSummaryState TableRow( children: [ - const _Prefix(type: FiroType.public), + const _Prefix(type: BalanceType.public), _Balance(coin: coin, amount: balanceToShowPublic), if (price != null) _Price( @@ -147,13 +147,13 @@ class _WFiroDesktopWalletSummaryState class _Prefix extends StatelessWidget { const _Prefix({super.key, required this.type}); - final FiroType type; + final BalanceType type; String get asset { switch (type) { - case FiroType.public: + case BalanceType.public: return Assets.png.glasses; - case FiroType.spark: + case BalanceType.private: return Assets.svg.spark; } } 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 ab48068b4..20f2524a6 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 @@ -16,8 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../../../../../db/sqlite/firo_cache.dart'; -import '../../../../../providers/db/main_db_provider.dart'; -import '../../../../../providers/global/wallets_provider.dart'; +import '../../../../../providers/providers.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../themes/theme_providers.dart'; import '../../../../../utilities/assets.dart'; @@ -25,6 +24,7 @@ import '../../../../../utilities/text_styles.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/wallet_mixin_interfaces/mweb_interface.dart'; import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; @@ -69,7 +69,8 @@ class _MoreFeaturesDialogState extends ConsumerState { } } - late final DSBController _switchController; + late final DSBController _switchControllerAddressReuse; + late final DSBController _switchControllerMwebToggle; bool _switchReuseAddressToggledLock = false; // Mutex. Future _switchReuseAddressToggled() async { @@ -79,7 +80,7 @@ class _MoreFeaturesDialogState extends ConsumerState { _switchReuseAddressToggledLock = true; // Lock mutex. try { - if (_switchController.isOn?.call() != true) { + if (_switchControllerAddressReuse.isOn?.call() != true) { final canContinue = await showDialog( context: context, builder: (context) { @@ -168,16 +169,135 @@ class _MoreFeaturesDialogState extends ConsumerState { isar: ref.read(mainDBProvider).isar, ); - if (_switchController.isOn != null) { - if (_switchController.isOn!.call() != shouldReuse) { - _switchController.activate?.call(); + if (_switchControllerAddressReuse.isOn != null) { + if (_switchControllerAddressReuse.isOn!.call() != shouldReuse) { + _switchControllerAddressReuse.activate?.call(); + } + } + } + + bool _switchMwebToggleToggledLock = false; // Mutex. + Future _switchMwebToggleToggled() async { + if (_switchMwebToggleToggledLock) { + return; + } + _switchMwebToggleToggledLock = true; // Lock mutex. + + try { + if (_switchControllerMwebToggle.isOn?.call() != true) { + final canContinue = await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Notice", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Activating MWEB requires synchronizing on-chain MWEB related data. " + "This currently requires about 800 MB of storage.", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox(height: 43), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + ), + const SizedBox(width: 16), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ); + + if (canContinue == true) { + await _updateMwebToggle(true); + + unawaited( + (ref.read(pWallets).getWallet(widget.walletId) as MwebInterface) + .open(), + ); + } + } else { + await _updateMwebToggle(false); + } + } finally { + // ensure _switchMwebToggleToggledLock is set to false no matter what. + _switchMwebToggleToggledLock = false; + } + } + + Future _updateMwebToggle(bool value) async { + if (value) { + unawaited( + ref + .read(pMwebService) + .initService(ref.read(pWalletCoin(widget.walletId)).network), + ); + } + + await ref + .read(pWalletInfo(widget.walletId)) + .updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: value}, + isar: ref.read(mainDBProvider).isar, + ); + + if (_switchControllerMwebToggle.isOn != null) { + if (_switchControllerMwebToggle.isOn!.call() != value) { + _switchControllerMwebToggle.activate?.call(); } } } @override void initState() { - _switchController = DSBController(); + _switchControllerAddressReuse = DSBController(); + _switchControllerMwebToggle = DSBController(); super.initState(); } @@ -281,7 +401,7 @@ class _MoreFeaturesDialogState extends ConsumerState { )[WalletInfoKeys.reuseAddress] as bool? ?? false, - controller: _switchController, + controller: _switchControllerAddressReuse, ), ), ), @@ -299,6 +419,43 @@ class _MoreFeaturesDialogState extends ConsumerState { ), ); + case WalletFeature.enableMweb: + return _MoreFeaturesItemBase( + onPressed: _switchMwebToggleToggled, + 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.mwebEnabled] + as bool? ?? + false, + controller: _switchControllerMwebToggle, + ), + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enable MWEB", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ); + default: return _MoreFeaturesItem( label: option.$1.label, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart new file mode 100644 index 000000000..6ce1d19e4 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/mweb_desktop_wallet_summary.dart @@ -0,0 +1,206 @@ +/* + * 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-06-13 + * + */ + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import '../../../../providers/providers.dart'; +import '../../../../providers/wallet/wallet_balance_toggle_state_provider.dart'; +import '../../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/amount/amount.dart'; +import '../../../../utilities/amount/amount_formatter.dart'; +import '../../../../utilities/enums/wallet_balance_toggle_state.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../../../wallets/isar/providers/wallet_info_provider.dart'; +import 'desktop_balance_toggle_button.dart'; + +class MwebDesktopWalletSummary extends ConsumerStatefulWidget { + const MwebDesktopWalletSummary({ + super.key, + required this.walletId, + required this.initialSyncStatus, + }); + + final String walletId; + final WalletSyncStatus initialSyncStatus; + + @override + ConsumerState createState() => + _WMwebDesktopWalletSummaryState(); +} + +class _WMwebDesktopWalletSummaryState + extends ConsumerState { + late final String walletId; + + late final CryptoCurrency coin; + late final bool isMweb; + + @override + void initState() { + super.initState(); + walletId = widget.walletId; + coin = ref.read(pWalletCoin(widget.walletId)); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + Decimal? price; + if (ref.watch( + prefsChangeNotifierProvider.select((value) => value.externalCalls), + )) { + price = + ref + .watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getPrice(coin), + ), + ) + ?.value; + } + + final _showAvailable = + ref.watch(walletBalanceToggleStateProvider.state).state == + WalletBalanceToggleState.available; + + final balance0 = ref.watch(pWalletBalanceSecondary(walletId)); + final balanceToShowSpark = + _showAvailable ? balance0.spendable : balance0.total; + + final balance2 = ref.watch(pWalletBalance(walletId)); + final balanceToShowPublic = + _showAvailable ? balance2.spendable : balance2.total; + + return Consumer( + builder: (context, ref, __) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Table( + columnWidths: { + 0: const IntrinsicColumnWidth(), + 1: const IntrinsicColumnWidth(), + if (price != null) 2: const IntrinsicColumnWidth(), + }, + children: [ + TableRow( + children: [ + const _Prefix(isMweb: true), + _Balance(coin: coin, amount: balanceToShowSpark), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowSpark, + price: price, + ), + ], + ), + + TableRow( + children: [ + const _Prefix(isMweb: false), + _Balance(coin: coin, amount: balanceToShowPublic), + if (price != null) + _Price( + coin: coin, + amount: balanceToShowPublic, + price: price, + ), + ], + ), + ], + ), + + const SizedBox(width: 8), + WalletRefreshButton( + walletId: walletId, + initialSyncStatus: widget.initialSyncStatus, + ), + const SizedBox(width: 8), + const DesktopBalanceToggleButton(), + ], + ); + }, + ); + } +} + +class _Prefix extends StatelessWidget { + const _Prefix({super.key, required this.isMweb}); + + final bool isMweb; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SelectableText( + isMweb ? "Private" : "Public", + style: STextStyles.w500_24(context), + ), + ], + ), + ); + } +} + +class _Balance extends ConsumerWidget { + const _Balance({super.key, required this.coin, required this.amount}); + + final CryptoCurrency coin; + final Amount amount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SelectableText( + ref.watch(pAmountFormatter(coin)).format(amount, ethContract: null), + style: STextStyles.desktopH3(context), + textAlign: TextAlign.end, + ); + } +} + +class _Price extends ConsumerWidget { + const _Price({ + super.key, + required this.coin, + required this.amount, + required this.price, + }); + + final CryptoCurrency coin; + final Amount amount; + final Decimal price; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: SelectableText( + "${Amount.fromDecimal(price * amount.decimal, fractionDigits: 2).fiatString(locale: ref.watch(localeServiceChangeNotifierProvider.select((value) => value.locale)))} " + "${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + + textAlign: TextAlign.end, + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart index 7fbcef510..7a7bc88ca 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart @@ -30,11 +30,7 @@ import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; class NetworkInfoButton extends ConsumerStatefulWidget { - const NetworkInfoButton({ - super.key, - required this.walletId, - this.eventBus, - }); + const NetworkInfoButton({super.key, required this.walletId, this.eventBus}); final String walletId; final EventBus? eventBus; @@ -74,27 +70,25 @@ class _NetworkInfoButtonState extends ConsumerState { } } - _syncStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentSyncStatus = event.newStatus; - }); - } - }, - ); + _syncStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentSyncStatus = event.newStatus; + }); + } + }); - _nodeStatusSubscription = - eventBus.on().listen( - (event) async { - if (event.walletId == widget.walletId) { - setState(() { - _currentNodeStatus = event.newStatus; - }); - } - }, - ); + _nodeStatusSubscription = eventBus + .on() + .listen((event) async { + if (event.walletId == widget.walletId) { + setState(() { + _currentNodeStatus = event.newStatus; + }); + } + }); super.initState(); } @@ -151,9 +145,9 @@ class _NetworkInfoButtonState extends ConsumerState { return Text( label, - style: STextStyles.desktopMenuItemSelected(context).copyWith( - color: _getColor(status, context), - ), + style: STextStyles.desktopMenuItemSelected( + context, + ).copyWith(color: _getColor(status, context)), ); } @@ -172,9 +166,7 @@ class _NetworkInfoButtonState extends ConsumerState { Widget build(BuildContext context) { return RawMaterialButton( hoverColor: _getColor(_currentSyncStatus, context).withOpacity(0.1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(1000), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(1000)), onPressed: () { if (Util.isDesktop) { // showDialog( @@ -220,88 +212,80 @@ class _NetworkInfoButtonState extends ConsumerState { showDialog( context: context, - builder: (context) => Navigator( - initialRoute: WalletNetworkSettingsView.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, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Network", - style: STextStyles.desktopH3(context), - ), - DesktopDialogCloseButton( - onPressedOverride: Navigator.of( - context, - rootNavigator: true, - ).pop, + builder: + (context) => Navigator( + initialRoute: WalletNetworkSettingsView.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), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Network", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: + Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], ), - ], - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.only( - top: 16, - left: 32, - right: 32, - bottom: 32, ), - child: SingleChildScrollView( - child: WalletNetworkSettingsView( - walletId: walletId, - initialSyncStatus: _currentSyncStatus, - initialNodeStatus: _currentNodeStatus, + Flexible( + child: Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 32, + ), + child: SingleChildScrollView( + child: WalletNetworkSettingsView( + walletId: walletId, + initialSyncStatus: _currentSyncStatus, + initialNodeStatus: _currentNodeStatus, + ), + ), ), ), - ), + ], ), - ], + ), + const RouteSettings( + name: WalletNetworkSettingsView.routeName, + ), ), - ), - const RouteSettings( - name: WalletNetworkSettingsView.routeName, - ), - ), - ]; - }, - ), + ]; + }, + ), ); } else { Navigator.of(context).pushNamed( WalletNetworkSettingsView.routeName, - arguments: Tuple3( - walletId, - _currentSyncStatus, - _currentNodeStatus, - ), + arguments: Tuple3(walletId, _currentSyncStatus, _currentNodeStatus), ); } }, child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16, - horizontal: 32, - ), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 32), child: Row( children: [ _buildNetworkIcon(_currentSyncStatus, context), - const SizedBox( - width: 6, - ), + const SizedBox(width: 6), _buildText(_currentSyncStatus, context), ], ), diff --git a/lib/providers/global/mweb_service_provider.dart b/lib/providers/global/mweb_service_provider.dart new file mode 100644 index 000000000..1cddcb407 --- /dev/null +++ b/lib/providers/global/mweb_service_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../services/mwebd_service.dart'; + +final pMwebService = StateProvider( + (ref) => MwebdService.instance, +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 3db18af2f..d86e4de3b 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -23,6 +23,7 @@ export './global/barcode_scanner_provider.dart'; export './global/clipboard_provider.dart'; export './global/duress_provider.dart'; export './global/locale_provider.dart'; +export './global/mweb_service_provider.dart'; export './global/node_service_provider.dart'; export './global/notifications_provider.dart'; export './global/prefs_provider.dart'; diff --git a/lib/providers/ui/preview_tx_button_state_provider.dart b/lib/providers/ui/preview_tx_button_state_provider.dart index 35daf888f..9ff71aca7 100644 --- a/lib/providers/ui/preview_tx_button_state_provider.dart +++ b/lib/providers/ui/preview_tx_button_state_provider.dart @@ -27,13 +27,13 @@ final pPreviewTxButtonEnabled = Provider.autoDispose if (coin is Firo) { final firoType = ref.watch(publicPrivateBalanceStateProvider); switch (firoType) { - case FiroType.spark: + case BalanceType.private: return (ref.watch(pValidSendToAddress) || ref.watch(pValidSparkSendToAddress)) && !ref.watch(pIsExchangeAddress) && amount > Amount.zero; - case FiroType.public: + case BalanceType.public: return ref.watch(pValidSendToAddress) && amount > Amount.zero; } } else { diff --git a/lib/providers/wallet/public_private_balance_state_provider.dart b/lib/providers/wallet/public_private_balance_state_provider.dart index 7484db9f0..39ce0ae88 100644 --- a/lib/providers/wallet/public_private_balance_state_provider.dart +++ b/lib/providers/wallet/public_private_balance_state_provider.dart @@ -10,8 +10,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -enum FiroType { public, spark } +enum BalanceType { public, private } -final publicPrivateBalanceStateProvider = StateProvider( - (_) => FiroType.spark, +final publicPrivateBalanceStateProvider = StateProvider( + (_) => BalanceType.private, ); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7c0f4ea13..3266d1273 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -178,6 +178,7 @@ import 'pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'pages_desktop_specific/desktop_home_view.dart'; +import 'pages_desktop_specific/mweb_utxos_view.dart'; import 'pages_desktop_specific/my_stack_view/my_stack_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; @@ -2227,6 +2228,16 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MwebUtxosView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MwebUtxosView(walletId: args), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopCoinControlView.routeName: if (args is String) { return getRoute( diff --git a/lib/services/frost.dart b/lib/services/frost.dart index 76c0b5db9..33066de92 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -12,12 +12,11 @@ import '../utilities/amount/amount.dart'; import '../utilities/extensions/extensions.dart'; import '../utilities/logger.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; +import '../wallets/models/tx_recipient.dart'; abstract class Frost { //==================== utility =============================================== - static List getParticipants({ - required String multisigConfig, - }) { + static List getParticipants({required String multisigConfig}) { try { final numberOfParticipants = multisigParticipants( multisigConfig: multisigConfig, @@ -26,10 +25,7 @@ abstract class Frost { final List participants = []; for (int i = 0; i < numberOfParticipants; i++) { participants.add( - multisigParticipant( - multisigConfig: multisigConfig, - index: i, - ), + multisigParticipant(multisigConfig: multisigConfig, index: i), ); } @@ -45,18 +41,18 @@ abstract class Frost { decodeMultisigConfig(multisigConfig: encodedConfig); return true; } catch (e, s) { - Logging.instance.f("validateEncodedMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "validateEncodedMultisigConfig failed: ", + error: e, + stackTrace: s, + ); return false; } } - static int getThreshold({ - required String multisigConfig, - }) { + static int getThreshold({required String multisigConfig}) { try { - final threshold = multisigThreshold( - multisigConfig: multisigConfig, - ); + final threshold = multisigThreshold(multisigConfig: multisigConfig); return threshold; } catch (e, s) { @@ -70,7 +66,8 @@ abstract class Frost { String changeAddress, int feePerWeight, List inputs, - }) extractDataFromSignConfig({ + }) + extractDataFromSignConfig({ required String serializedKeys, required String signConfig, required CryptoCurrency coin, @@ -85,8 +82,9 @@ abstract class Frost { ); // get various data from config - final feePerWeight = - signFeePerWeight(signConfigPointer: signConfigPointer); + final feePerWeight = signFeePerWeight( + signConfigPointer: signConfigPointer, + ); final changeAddress = signChange(signConfigPointer: signConfigPointer); final recipientsCount = signPayments( signConfigPointer: signConfigPointer, @@ -103,15 +101,13 @@ abstract class Frost { signConfigPointer: signConfigPointer, index: i, ); - recipients.add( - ( - address: address, - amount: Amount( - rawValue: BigInt.from(amount), - fractionDigits: coin.fractionDigits, - ), + recipients.add(( + address: address, + amount: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.fractionDigits, ), - ); + )); } // get utxos @@ -135,7 +131,11 @@ abstract class Frost { inputs: outputs, ); } catch (e, s) { - Logging.instance.f("extractDataFromSignConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractDataFromSignConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -156,7 +156,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createMultisigConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createMultisigConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -166,10 +170,8 @@ abstract class Frost { String commitments, Pointer multisigConfigWithNamePtr, Pointer secretShareMachineWrapperPtr, - }) startKeyGeneration({ - required String multisigConfig, - required String myName, - }) { + }) + startKeyGeneration({required String multisigConfig, required String myName}) { try { final startKeyGenResPtr = startKeyGen( multisigConfig: multisigConfig, @@ -189,15 +191,17 @@ abstract class Frost { secretShareMachineWrapperPtr: machinePtr, ); } catch (e, s) { - Logging.instance.f("startKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "startKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String share, - Pointer secretSharesResPtr, - }) generateSecretShares({ + static ({String share, Pointer secretSharesResPtr}) + generateSecretShares({ required Pointer multisigConfigWithNamePtr, required String mySeed, required Pointer secretShareMachineWrapperPtr, @@ -216,16 +220,17 @@ abstract class Frost { return (share: share, secretSharesResPtr: secretSharesResPtr); } catch (e, s) { - Logging.instance.f("generateSecretShares failed: ", error: e, stackTrace: s); + Logging.instance.f( + "generateSecretShares failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - Uint8List multisigId, - String recoveryString, - String serializedKeys, - }) completeKeyGeneration({ + static ({Uint8List multisigId, String recoveryString, String serializedKeys}) + completeKeyGeneration({ required Pointer multisigConfigWithNamePtr, required Pointer secretSharesResPtr, required List shares, @@ -254,7 +259,11 @@ abstract class Frost { serializedKeys: serializedKeys, ); } catch (e, s) { - Logging.instance.f("completeKeyGeneration failed: ", error: e, stackTrace: s); + Logging.instance.f( + "completeKeyGeneration failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -265,13 +274,14 @@ abstract class Frost { required String serializedKeys, required int network, required List< - ({ - UTXO utxo, - Uint8List scriptPubKey, - AddressDerivationData addressDerivationData - })> - inputs, - required List<({String address, Amount amount, bool isChange})> outputs, + ({ + UTXO utxo, + Uint8List scriptPubKey, + AddressDerivationData addressDerivationData, + }) + > + inputs, + required List outputs, required String changeAddress, required int feePerWeight, }) { @@ -279,17 +289,18 @@ abstract class Frost { final signConfig = newSignConfig( thresholdKeysWrapperPointer: deserializeKeys(keys: serializedKeys), network: network, - outputs: inputs - .map( - (e) => Output( - hash: e.utxo.txid.toUint8ListFromHex, - vout: e.utxo.vout, - value: e.utxo.value, - scriptPubKey: e.scriptPubKey, - addressDerivationData: e.addressDerivationData, - ), - ) - .toList(), + outputs: + inputs + .map( + (e) => Output( + hash: e.utxo.txid.toUint8ListFromHex, + vout: e.utxo.vout, + value: e.utxo.value, + scriptPubKey: e.scriptPubKey, + addressDerivationData: e.addressDerivationData, + ), + ) + .toList(), paymentAddresses: outputs.map((e) => e.address).toList(), paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(), change: changeAddress, @@ -306,7 +317,8 @@ abstract class Frost { static ({ Pointer machinePtr, String preprocess, - }) attemptSignConfig({ + }) + attemptSignConfig({ required int network, required String config, required String serializedKeys, @@ -333,7 +345,8 @@ abstract class Frost { static ({ Pointer machinePtr, String share, - }) continueSigning({ + }) + continueSigning({ required Pointer machinePtr, required List preprocesses, }) { @@ -358,10 +371,7 @@ abstract class Frost { required List shares, }) { try { - final rawTransaction = completeSign( - machine: machinePtr, - shares: shares, - ); + final rawTransaction = completeSign(machine: machinePtr, shares: shares); return rawTransaction; } catch (e, s) { @@ -404,28 +414,24 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("createResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "createResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static ({ - String resharerStart, - Pointer machine, - }) beginResharer({ - required String serializedKeys, - required String config, - }) { + static ({String resharerStart, Pointer machine}) + beginResharer({required String serializedKeys, required String config}) { try { final result = startResharer( serializedKeys: serializedKeys, config: config, ); - return ( - resharerStart: result.encoded, - machine: result.machine, - ); + return (resharerStart: result.encoded, machine: result.machine); } catch (e, s) { Logging.instance.f("beginResharer failed: ", error: e, stackTrace: s); rethrow; @@ -433,10 +439,8 @@ abstract class Frost { } /// expects [resharerStarts] of length equal to resharers. - static ({ - String resharedStart, - Pointer prior, - }) beginReshared({ + static ({String resharedStart, Pointer prior}) + beginReshared({ required String myName, required String resharerConfig, required List resharerStarts, @@ -448,10 +452,7 @@ abstract class Frost { resharerConfig: resharerConfig, resharerStarts: resharerStarts, ); - return ( - resharedStart: result.encoded, - prior: result.machine, - ); + return (resharedStart: result.encoded, prior: result.machine); } catch (e, s) { Logging.instance.f("beginReshared failed: ", error: e, stackTrace: s); rethrow; @@ -476,11 +477,8 @@ abstract class Frost { } /// expects [resharerCompletes] of length equal to resharers - static ({ - String multisigConfig, - String serializedKeys, - String resharedId, - }) finishReshared({ + static ({String multisigConfig, String serializedKeys, String resharedId}) + finishReshared({ required StartResharedRes prior, required List resharerCompletes, }) { @@ -504,7 +502,11 @@ abstract class Frost { return config; } catch (e, s) { - Logging.instance.f("decodedResharerConfig failed: ", error: e, stackTrace: s); + Logging.instance.f( + "decodedResharerConfig failed: ", + error: e, + stackTrace: s, + ); rethrow; } } @@ -513,9 +515,8 @@ abstract class Frost { int newThreshold, Map resharers, List newParticipants, - }) extractResharerConfigData({ - required String rConfig, - }) { + }) + extractResharerConfigData({required String rConfig}) { final decoded = _decodeRConfigWithResharers(rConfig); final resharerConfig = decoded.config; @@ -564,8 +565,9 @@ abstract class Frost { for (final resharer in resharers) { resharersMap[decoded.resharers.entries - .firstWhere((e) => e.value == resharer) - .key] = resharer; + .firstWhere((e) => e.value == resharer) + .key] = + resharer; } return ( @@ -574,28 +576,25 @@ abstract class Frost { newParticipants: newParticipants, ); } catch (e, s) { - Logging.instance.f("extractResharerConfigData failed: ", error: e, stackTrace: s); + Logging.instance.f( + "extractResharerConfigData failed: ", + error: e, + stackTrace: s, + ); rethrow; } } - static String encodeRConfig( - String config, - Map resharers, - ) { + static String encodeRConfig(String config, Map resharers) { return base64Encode("$config@${jsonEncode(resharers)}".toUint8ListFromUtf8); } - static String decodeRConfig( - String rConfig, - ) { + static String decodeRConfig(String rConfig) { return base64Decode(rConfig).toUtf8String.split("@").first; } static ({Map resharers, String config}) - _decodeRConfigWithResharers( - String rConfig, - ) { + _decodeRConfigWithResharers(String rConfig) { final parts = base64Decode(rConfig).toUtf8String.split("@"); final config = parts[0]; diff --git a/lib/services/mwebd_service.dart b/lib/services/mwebd_service.dart new file mode 100644 index 000000000..f8b3a6a32 --- /dev/null +++ b/lib/services/mwebd_service.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter_mwebd/flutter_mwebd.dart'; +import 'package:mutex/mutex.dart'; +import 'package:mweb_client/mweb_client.dart'; + +import '../utilities/logger.dart'; +import '../utilities/prefs.dart'; +import '../utilities/stack_file_system.dart'; +import '../wallets/crypto_currency/crypto_currency.dart'; +import 'event_bus/events/global/tor_connection_status_changed_event.dart'; +import 'event_bus/events/global/tor_status_changed_event.dart'; +import 'event_bus/global_event_bus.dart'; +import 'tor_service.dart'; + +final class MwebdService { + static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { + CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", + CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + + final Map + _map = {}; + + late final StreamSubscription + _torStatusListener; + late final StreamSubscription + _torPreferenceListener; + + final Mutex _torConnectingLock = Mutex(); + + static final instance = MwebdService._(); + + MwebdService._() { + 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(); + } + break; + + case TorConnectionStatus.connected: + case TorConnectionStatus.disconnected: + if (_torConnectingLock.isLocked) { + _torConnectingLock.release(); + } + break; + } + }); + + // Listen for tor preference changes. + _torPreferenceListener = bus.on().listen(( + event, + ) async { + if (Prefs.instance.useTor) { + return await _torConnectingLock.protect(() async { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + return await _update(proxyInfo); + }); + } else { + return await _update(null); + } + }); + } + + // locked while mweb servers and clients are updating + final _updateLock = Mutex(); + + // update function called when Tor pref changed + Future _update(({InternetAddress host, int port})? proxyInfo) async { + await _updateLock.protect(() async { + final proxy = + proxyInfo == null + ? "" + : "${proxyInfo.host.address}:${proxyInfo.port}"; + final nets = _map.keys; + for (final net in nets) { + final old = _map.remove(net)!; + + await old.client.cleanup(); + await old.server.stopServer(); + + final port = await _getRandomUnusedPort(); + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final newServer = MwebdServer( + chain: old.server.chain, + dataDir: old.server.dataDir, + peer: old.server.peer, + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost( + "127.0.0.1", + newServer.serverPort, + ); + + _map[net] = (server: newServer, client: newClient); + } + }); + } + + Future initService(CryptoCurrencyNetwork net) async { + Logging.instance.i("MwebdService init($net) called..."); + await _updateLock.protect(() async { + if (_map[net] != null) { + Logging.instance.i("MwebdService init($net) was already called."); + return; + } + + if (_map.isNotEmpty) { + for (final old in _map.values) { + try { + await old.client.cleanup(); + await old.server.stopServer(); + } catch (e, s) { + Logging.instance.i( + "Switching mwebd chain. Error likely expected here.", + error: e, + stackTrace: s, + ); + } + } + _map.clear(); + } + + final port = await _getRandomUnusedPort(); + + if (port == null) { + throw Exception("Could not find an unused port for mwebd"); + } + + final chain = switch (net) { + CryptoCurrencyNetwork.main => "mainnet", + CryptoCurrencyNetwork.test => "testnet", + CryptoCurrencyNetwork.stage => throw UnimplementedError(), + CryptoCurrencyNetwork.test4 => throw UnimplementedError(), + }; + + final dir = await StackFileSystem.applicationMwebdDirectory(chain); + + final String proxy; + if (Prefs.instance.useTor) { + final proxyInfo = TorService.sharedInstance.getProxyInfo(); + proxy = "${proxyInfo.host.address}:${proxyInfo.port}"; + } else { + proxy = ""; + } + + final newServer = MwebdServer( + chain: chain, + dataDir: dir.path, + peer: defaultPeer(net), + proxy: proxy, + serverPort: port, + ); + await newServer.createServer(); + await newServer.startServer(); + + final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); + + _map[net] = (server: newServer, client: newClient); + + Logging.instance.i("MwebdService init($net) completed!"); + }); + } + + /// Get server status. Returns null if no server was initialized. + Future getServerStatus(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return await _map[net]?.server.getStatus(); + }); + } + + /// Get client for network. Returns null if no server was initialized. + Future getClient(CryptoCurrencyNetwork net) async { + return await _updateLock.protect(() async { + return _map[net]?.client; + }); + } + + Future> logsStream( + CryptoCurrencyNetwork net, { + Duration pollInterval = const Duration(milliseconds: 200), + }) async { + final controller = StreamController(); + int offset = 0; + String leftover = ''; + Timer? timer; + + final path = + "${(await StackFileSystem.applicationMwebdDirectory(net == CryptoCurrencyNetwork.main ? "mainnet" : "testnet")).path}" + "${Platform.pathSeparator}logs" + "${Platform.pathSeparator}debug.log"; + + Future poll() async { + if (!controller.isClosed) { + final file = File(path); + final length = await file.length(); + + if (length > offset) { + final raf = await file.open(); + await raf.setPosition(offset); + final bytes = await raf.read(length - offset); + await raf.close(); + + final chunk = utf8.decode(bytes); + final lines = (leftover + chunk).split('\n'); + leftover = lines.removeLast(); // possibly incomplete + + for (final line in lines) { + controller.add(line); + } + + offset = length; + } + } + } + + timer = Timer.periodic(pollInterval, (_) => poll()); + + controller.onCancel = () { + timer?.cancel(); + controller.close(); + }; + + return controller.stream; + } +} + +// ============================================================================ +Future _getRandomUnusedPort({Set excluded = const {}}) async { + const int minPort = 1024; + const int maxPort = 65535; + const int maxAttempts = 1000; + + final random = Random.secure(); + + for (int i = 0; i < maxAttempts; i++) { + final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); + + if (excluded.contains(potentialPort)) { + continue; + } + + try { + final ServerSocket socket = await ServerSocket.bind( + InternetAddress.anyIPv4, + potentialPort, + ); + await socket.close(); + return potentialPort; + } catch (_) { + excluded.add(potentialPort); + continue; + } + } + + return null; +} + +// final class MwebdService { +// static String defaultPeer(CryptoCurrencyNetwork net) => switch (net) { +// CryptoCurrencyNetwork.main => "litecoin.stackwallet.com:9333", +// CryptoCurrencyNetwork.test => "litecoin.stackwallet.com:19335", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final Map +// _map = {}; +// +// late final StreamSubscription +// _torStatusListener; +// late final StreamSubscription +// _torPreferenceListener; +// +// final Mutex _torConnectingLock = Mutex(); +// +// static final instance = MwebdService._(); +// +// MwebdService._() { +// 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(); +// } +// break; +// +// case TorConnectionStatus.connected: +// case TorConnectionStatus.disconnected: +// if (_torConnectingLock.isLocked) { +// _torConnectingLock.release(); +// } +// break; +// } +// }); +// +// // Listen for tor preference changes. +// _torPreferenceListener = bus.on().listen(( +// event, +// ) async { +// if (Prefs.instance.useTor) { +// return await _torConnectingLock.protect(() async { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// return await _update(proxyInfo); +// }); +// } else { +// return await _update(null); +// } +// }); +// } +// +// // locked while mweb servers and clients are updating +// final _updateLock = Mutex(); +// +// // update function called when Tor pref changed +// Future _update(({InternetAddress host, int port})? proxyInfo) async { +// await _updateLock.protect(() async { +// final proxy = +// proxyInfo == null +// ? "" +// : "${proxyInfo.host.address}:${proxyInfo.port}"; +// final nets = _map.keys; +// for (final net in nets) { +// final old = _map.remove(net)!; +// +// await old.client.cleanup(); +// await old.server.stopServer(); +// +// final port = await _getRandomUnusedPort(); +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final newServer = MwebdServer( +// chain: old.server.chain, +// dataDir: old.server.dataDir, +// peer: old.server.peer, +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost( +// "127.0.0.1", +// newServer.serverPort, +// ); +// +// _map[net] = (server: newServer, client: newClient); +// } +// }); +// } +// +// Future init(CryptoCurrencyNetwork net) async { +// if (net == CryptoCurrencyNetwork.test) return; +// +// Logging.instance.i("MwebdService init($net) called..."); +// await _updateLock.protect(() async { +// if (_map[net] != null) { +// Logging.instance.i("MwebdService init($net) was already called."); +// return; +// } +// +// final port = await _getRandomUnusedPort(); +// +// if (port == null) { +// throw Exception("Could not find an unused port for mwebd"); +// } +// +// final chain = switch (net) { +// CryptoCurrencyNetwork.main => "mainnet", +// CryptoCurrencyNetwork.test => "testnet", +// CryptoCurrencyNetwork.stage => throw UnimplementedError(), +// CryptoCurrencyNetwork.test4 => throw UnimplementedError(), +// }; +// +// final dir = await StackFileSystem.applicationMwebdDirectory(chain); +// +// final String proxy; +// if (Prefs.instance.useTor) { +// final proxyInfo = TorService.sharedInstance.getProxyInfo(); +// proxy = "${proxyInfo.host.address}:${proxyInfo.port}"; +// } else { +// proxy = ""; +// } +// +// final newServer = MwebdServer( +// chain: chain, +// dataDir: dir.path, +// peer: defaultPeer(net), +// proxy: proxy, +// serverPort: port, +// ); +// await newServer.createServer(); +// await newServer.startServer(); +// +// final newClient = MwebClient.fromHost("127.0.0.1", newServer.serverPort); +// +// _map[net] = (server: newServer, client: newClient); +// +// Logging.instance.i("MwebdService init($net) completed!"); +// }); +// } +// +// /// Get server status. Returns null if no server was initialized. +// Future getServerStatus(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return await _map[net]?.server.getStatus(); +// }); +// } +// +// /// Get client for network. Returns null if no server was initialized. +// Future getClient(CryptoCurrencyNetwork net) async { +// return await _updateLock.protect(() async { +// return _map[net]?.client; +// }); +// } +// } +// +// // ============================================================================ +// Future _getRandomUnusedPort({Set excluded = const {}}) async { +// const int minPort = 1024; +// const int maxPort = 65535; +// const int maxAttempts = 1000; +// +// final random = Random.secure(); +// +// for (int i = 0; i < maxAttempts; i++) { +// final int potentialPort = minPort + random.nextInt(maxPort - minPort + 1); +// +// if (excluded.contains(potentialPort)) { +// continue; +// } +// +// try { +// final ServerSocket socket = await ServerSocket.bind( +// InternetAddress.anyIPv4, +// potentialPort, +// ); +// await socket.close(); +// return potentialPort; +// } catch (_) { +// excluded.add(potentialPort); +// continue; +// } +// } +// +// return null; +// } diff --git a/lib/services/price.dart b/lib/services/price.dart index 8dc686fa1..9641d0328 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -49,6 +49,7 @@ class PriceAPI { Nano: "nano", Banano: "banano", Xelis: "xelis", + Salvium: "salvium", }; static const refreshInterval = 60; diff --git a/lib/utilities/barcode_scanner_interface.dart b/lib/utilities/barcode_scanner_interface.dart index 87b079d03..f8256f2e5 100644 --- a/lib/utilities/barcode_scanner_interface.dart +++ b/lib/utilities/barcode_scanner_interface.dart @@ -10,27 +10,37 @@ import 'dart:io'; -import 'package:barcode_scan2/barcode_scan2.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import '../widgets/desktop/primary_button.dart'; import '../widgets/desktop/secondary_button.dart'; +import '../widgets/qr_scanner.dart'; import '../widgets/stack_dialog.dart'; import 'logger.dart'; +class ScanResult { + final String rawContent; + + ScanResult({required this.rawContent}); +} + abstract class BarcodeScannerInterface { - Future scan({ScanOptions options = const ScanOptions()}); + Future scan({required BuildContext context}); } class BarcodeScannerWrapper implements BarcodeScannerInterface { const BarcodeScannerWrapper(); @override - Future scan({ScanOptions options = const ScanOptions()}) async { + Future scan({required BuildContext context}) async { try { - final result = await BarcodeScanner.scan(options: options); - return result; + final data = await showDialog( + context: context, + builder: (context) => const QrScanner(), + ); + + return ScanResult(rawContent: data.toString()); } catch (e) { rethrow; } diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 7a254dffe..e13557739 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -139,6 +139,15 @@ abstract class StackFileSystem { } } + static Future applicationMwebdDirectory(String network) async { + final root = await applicationRootDirectory(); + final dir = Directory(path.join(root.path, "mwebd", network)); + if (!dir.existsSync()) { + await dir.create(recursive: true); + } + return dir; + } + static Future applicationFiroCacheSQLiteDirectory() async { final root = await applicationRootDirectory(); if (_createSubDirs) { diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index 2a62acb57..eccb6fe38 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -102,4 +102,12 @@ class Banano extends NanoCurrency { throw UnsupportedError( "$runtimeType does not use bitcoin style derivation paths", ); + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.banano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index 330d3cb86..3edaea351 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import '../../../models/isar/models/blockchain_data/address.dart'; @@ -247,4 +248,20 @@ class BitcoinFrost extends FrostCurrency { // @override BigInt get defaultFeeRate => BigInt.from(1000); // https://github.com/bitcoin/bitcoin/blob/feab35189bc00bc4cf15e9dcb5cf6b34ff3a1e91/test/functional/mempool_limit.py#L259 + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + return switch (clAddress) { + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2WSHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + _ => null, + }; + } catch (_) { + return null; + } + } } diff --git a/lib/wallets/crypto_currency/coins/cardano.dart b/lib/wallets/crypto_currency/coins/cardano.dart index 976093a03..a59669eb7 100644 --- a/lib/wallets/crypto_currency/coins/cardano.dart +++ b/lib/wallets/crypto_currency/coins/cardano.dart @@ -128,4 +128,12 @@ class Cardano extends Bip39Currency { throw Exception("Unsupported network: $network"); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cardanoShelley; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/epiccash.dart b/lib/wallets/crypto_currency/coins/epiccash.dart index d35c0fb74..b715f9623 100644 --- a/lib/wallets/crypto_currency/coins/epiccash.dart +++ b/lib/wallets/crypto_currency/coins/epiccash.dart @@ -129,4 +129,12 @@ class Epiccash extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.mimbleWimble; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/ethereum.dart b/lib/wallets/crypto_currency/coins/ethereum.dart index f6f3601a1..448f4c9ff 100644 --- a/lib/wallets/crypto_currency/coins/ethereum.dart +++ b/lib/wallets/crypto_currency/coins/ethereum.dart @@ -113,4 +113,12 @@ class Ethereum extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.ethereum; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/litecoin.dart b/lib/wallets/crypto_currency/coins/litecoin.dart index 4ab8ec0c2..1b830c4f9 100644 --- a/lib/wallets/crypto_currency/coins/litecoin.dart +++ b/lib/wallets/crypto_currency/coins/litecoin.dart @@ -91,6 +91,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x0488ade4, pubHDPrefix: 0x0488b21e, bech32Hrp: "ltc", + mwebBech32Hrp: "ltcmweb", messagePrefix: '\x19Litecoin Signed Message:\n', minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently @@ -104,6 +105,7 @@ class Litecoin extends Bip39HDCurrency with ElectrumXCurrencyInterface { privHDPrefix: 0x04358394, pubHDPrefix: 0x043587cf, bech32Hrp: "tltc", + mwebBech32Hrp: "tmweb", messagePrefix: "\x19Litecoin Signed Message:\n", minFee: BigInt.from(1), // Not used in stack wallet currently minOutput: dustLimit.raw, // Not used in stack wallet currently diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 07f464926..04622c54d 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -102,4 +102,12 @@ class Nano extends NanoCurrency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.nano; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/salvium.dart b/lib/wallets/crypto_currency/coins/salvium.dart index d7618094c..db794dc53 100644 --- a/lib/wallets/crypto_currency/coins/salvium.dart +++ b/lib/wallets/crypto_currency/coins/salvium.dart @@ -116,7 +116,7 @@ class Salvium extends CryptonoteCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://explorer.salvium.io//tx/$txid"); + return Uri.parse("https://explorer.salvium.io/tx/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", diff --git a/lib/wallets/crypto_currency/coins/solana.dart b/lib/wallets/crypto_currency/coins/solana.dart index 39d243a1c..03331a922 100644 --- a/lib/wallets/crypto_currency/coins/solana.dart +++ b/lib/wallets/crypto_currency/coins/solana.dart @@ -122,4 +122,12 @@ class Solana extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.solana; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/stellar.dart b/lib/wallets/crypto_currency/coins/stellar.dart index 077564a50..1aa701f87 100644 --- a/lib/wallets/crypto_currency/coins/stellar.dart +++ b/lib/wallets/crypto_currency/coins/stellar.dart @@ -139,4 +139,12 @@ class Stellar extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.stellar; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/tezos.dart b/lib/wallets/crypto_currency/coins/tezos.dart index 78d3c02ad..179ae2ce1 100644 --- a/lib/wallets/crypto_currency/coins/tezos.dart +++ b/lib/wallets/crypto_currency/coins/tezos.dart @@ -220,4 +220,12 @@ class Tezos extends Bip39Currency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.tezos; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/coins/xelis.dart b/lib/wallets/crypto_currency/coins/xelis.dart index fc1eaaa20..62b33326b 100644 --- a/lib/wallets/crypto_currency/coins/xelis.dart +++ b/lib/wallets/crypto_currency/coins/xelis.dart @@ -141,4 +141,12 @@ class Xelis extends ElectrumCurrency { ); } } + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.xelis; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/crypto_currency.dart b/lib/wallets/crypto_currency/crypto_currency.dart index 4f6c232e6..0c2f2cb83 100644 --- a/lib/wallets/crypto_currency/crypto_currency.dart +++ b/lib/wallets/crypto_currency/crypto_currency.dart @@ -69,6 +69,7 @@ abstract class CryptoCurrency { String get genesisHash; bool validateAddress(String address); + AddressType? getAddressType(String address); NodeModel defaultNode({required bool isPrimary}); diff --git a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart index 387bf4454..c9d0da2b9 100644 --- a/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart +++ b/lib/wallets/crypto_currency/interfaces/electrumx_currency_interface.dart @@ -1,3 +1,6 @@ +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; + +import '../../../models/isar/models/blockchain_data/address.dart'; import '../intermediate/bip39_hd_currency.dart'; mixin ElectrumXCurrencyInterface on Bip39HDCurrency { @@ -5,4 +8,21 @@ mixin ElectrumXCurrencyInterface on Bip39HDCurrency { /// The default fee rate in satoshis per kilobyte. BigInt get defaultFeeRate; + + @override + AddressType? getAddressType(String address) { + try { + final clAddress = cl.Address.fromString(address, networkParams); + + return switch (clAddress) { + cl.P2PKHAddress() => AddressType.p2pkh, + cl.P2WSHAddress() => AddressType.p2sh, + cl.P2WPKHAddress() => AddressType.p2wpkh, + cl.MwebAddress() => AddressType.mweb, + _ => null, + }; + } catch (_) { + return null; + } + } } diff --git a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart index 319a501ac..a0069e09b 100644 --- a/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/cryptonote_currency.dart @@ -13,4 +13,12 @@ abstract class CryptonoteCurrency extends CryptoCurrency @override AddressType get defaultAddressType => AddressType.cryptonote; + + @override + AddressType? getAddressType(String address) { + if (validateAddress(address)) { + return AddressType.cryptonote; + } + return null; + } } diff --git a/lib/wallets/crypto_currency/intermediate/nano_currency.dart b/lib/wallets/crypto_currency/intermediate/nano_currency.dart index a04cd57a0..a553a6d6c 100644 --- a/lib/wallets/crypto_currency/intermediate/nano_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/nano_currency.dart @@ -1,4 +1,5 @@ import 'package:nanodart/nanodart.dart'; + import 'bip39_currency.dart'; abstract class NanoCurrency extends Bip39Currency { @@ -24,13 +25,10 @@ abstract class NanoCurrency extends Bip39Currency { List get possibleMnemonicLengths => [defaultSeedPhraseLength, 12]; @override - bool validateAddress(String address) => NanoAccounts.isValid( - nanoAccountType, - address, - ); + bool validateAddress(String address) => + NanoAccounts.isValid(nanoAccountType, address); @override - String get genesisHash => throw UnimplementedError( - "Not used in nano based coins", - ); + String get genesisHash => + throw UnimplementedError("Not used in nano based coins"); } diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index bb4dd4ab2..6df8dc456 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -139,6 +139,10 @@ class WalletInfo implements IsarId { bool get isDuressVisible => otherData[WalletInfoKeys.duressMarkedVisibleWalletKey] as bool? ?? false; + @ignore + bool get isMwebEnabled => + otherData[WalletInfoKeys.mwebEnabled] as bool? ?? false; + //============================================================================ //============= Updaters ================================================ @@ -392,6 +396,16 @@ class WalletInfo implements IsarId { ); } + Future setMwebEnabled({ + required bool newValue, + required Isar isar, + }) async { + await updateOtherData( + newEntries: {WalletInfoKeys.mwebEnabled: newValue}, + isar: isar, + ); + } + //============================================================================ WalletInfo({ @@ -505,4 +519,6 @@ abstract class WalletInfoKeys { static const String viewOnlyTypeIndexKey = "viewOnlyTypeIndexKey"; static const String duressMarkedVisibleWalletKey = "duressMarkedVisibleWalletKey"; + static const String mwebEnabled = "mwebEnabledKey"; + static const String mwebScanHeight = "mwebScanHeightKey"; } diff --git a/lib/wallets/isar/models/wallet_info.g.dart b/lib/wallets/isar/models/wallet_info.g.dart index 5e93564c0..3dda37c77 100644 --- a/lib/wallets/isar/models/wallet_info.g.dart +++ b/lib/wallets/isar/models/wallet_info.g.dart @@ -270,6 +270,8 @@ const _WalletInfomainAddressTypeEnumValueMap = { 'solana': 15, 'cardanoShelley': 16, 'xelis': 17, + 'fact0rn': 18, + 'mweb': 19, }; const _WalletInfomainAddressTypeValueEnumMap = { 0: AddressType.p2pkh, @@ -290,6 +292,8 @@ const _WalletInfomainAddressTypeValueEnumMap = { 15: AddressType.solana, 16: AddressType.cardanoShelley, 17: AddressType.xelis, + 18: AddressType.fact0rn, + 19: AddressType.mweb, }; Id _walletInfoGetId(WalletInfo object) { diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 28b985be4..9a8dffd21 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -3,6 +3,7 @@ import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; import 'package:tezart/tezart.dart' as tezart; import 'package:web3dart/web3dart.dart' as web3dart; +import '../../models/input.dart'; import '../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/paynym/paynym_account_lite.dart'; @@ -11,8 +12,23 @@ 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'; - -typedef TxRecipient = ({String address, Amount amount, bool isChange}); +import 'tx_recipient.dart'; + +export 'tx_recipient.dart'; + +enum TxType { + regular, + mweb, + mwebPegIn, + mwebPegOut; + + bool isMweb() => switch (this) { + TxType.mweb => true, + TxType.mwebPegIn => true, + TxType.mwebPegOut => true, + _ => false, + }; +} class TxData { final FeeRateType? feeRateType; @@ -33,8 +49,8 @@ class TxData { final String? memo; final List? recipients; - final Set? utxos; - final List? usedUTXOs; + final Set? utxos; + final List? usedUTXOs; final String? changeAddress; @@ -82,6 +98,8 @@ class TxData { // Namecoin Name related final NameOpState? opNameState; + final TxType type; + TxData({ this.feeRateType, this.feeRateAmount, @@ -116,6 +134,7 @@ class TxData { this.ignoreCachedBalanceChecks = false, this.opNameState, this.sparkNameInfo, + this.type = TxType.regular, }); Amount? get amount { @@ -229,8 +248,8 @@ class TxData { String? noteOnChain, String? memo, String? otherData, - Set? utxos, - List? usedUTXOs, + Set? utxos, + List? usedUTXOs, List? recipients, String? frostMSConfig, List? frostSigners, @@ -263,6 +282,7 @@ class TxData { int validBlocks, })? sparkNameInfo, + TxType? type, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -300,6 +320,7 @@ class TxData { ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, opNameState: opNameState ?? this.opNameState, sparkNameInfo: sparkNameInfo ?? this.sparkNameInfo, + type: type ?? this.type, ); } @@ -338,5 +359,6 @@ class TxData { 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' 'opNameState: $opNameState, ' 'sparkNameInfo: $sparkNameInfo, ' + 'type: $type, ' '}'; } diff --git a/lib/wallets/models/tx_recipient.dart b/lib/wallets/models/tx_recipient.dart index 8c5e9a9d4..9173864d3 100644 --- a/lib/wallets/models/tx_recipient.dart +++ b/lib/wallets/models/tx_recipient.dart @@ -1,11 +1,52 @@ +import '../../models/isar/models/blockchain_data/address.dart'; import '../../utilities/amount/amount.dart'; class TxRecipient { final String address; final Amount amount; + final bool isChange; + final AddressType addressType; TxRecipient({ required this.address, required this.amount, + required this.isChange, + required this.addressType, }); + + TxRecipient copyWith({ + String? address, + Amount? amount, + bool? isChange, + AddressType? addressType, + }) { + return TxRecipient( + address: address ?? this.address, + amount: amount ?? this.amount, + isChange: isChange ?? this.isChange, + addressType: addressType ?? this.addressType, + ); + } + + @override + String toString() { + return "TxRecipient{" + "address: $address, " + "amount: $amount, " + "isChange: $isChange, " + "addressType: $addressType" + "}"; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TxRecipient && + address == other.address && + amount == other.amount && + isChange == other.isChange && + addressType == other.addressType; + + @override + int get hashCode => Object.hash(address, amount, isChange, addressType); } diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index 3b0c36ac0..95f015c01 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -11,6 +11,7 @@ import 'package:isar/isar.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -235,7 +236,10 @@ class BitcoinFrostWallet extends Wallet } } - return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); + return txData.copyWith( + frostMSConfig: config, + utxos: utxosToUse.map((e) => StandardInput(e)).toSet(), + ); } catch (_) { rethrow; } @@ -676,11 +680,13 @@ class BitcoinFrostWallet extends Wallet Logging.instance.d("Sent txHash: $txHash"); // mark utxos as used - final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true)); + final usedUTXOs = txData.utxos!.whereType().map( + (e) => e.utxo.copyWith(used: true), + ); await mainDB.putUTXOs(usedUTXOs.toList()); txData = txData.copyWith( - utxos: usedUTXOs.toSet(), + utxos: usedUTXOs.map((e) => StandardInput(e)).toSet(), txHash: txHash, txid: txHash, ); diff --git a/lib/wallets/wallet/impl/cardano_wallet.dart b/lib/wallets/wallet/impl/cardano_wallet.dart index 2cad7c514..3909c5b4b 100644 --- a/lib/wallets/wallet/impl/cardano_wallet.dart +++ b/lib/wallets/wallet/impl/cardano_wallet.dart @@ -235,13 +235,11 @@ class CardanoWallet extends Bip39Wallet { // Check if we are sending all balance, which means no change and only one output for recipient. if (totalBalance == txData.amount!.raw) { final List newRecipients = [ - ( - address: txData.recipients!.first.address, + txData.recipients!.first.copyWith( amount: Amount( rawValue: txData.amount!.raw - fee, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: txData.recipients!.first.isChange, ), ]; return txData.copyWith( diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 215f41917..ea6654b12 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -631,8 +631,7 @@ class EpiccashWallet extends Bip39Wallet { throw Exception("Epic cash prepare send requires a single recipient!"); } - ({String address, Amount amount, bool isChange}) recipient = - txData.recipients!.first; + TxRecipient recipient = txData.recipients!.first; final int realFee = await _nativeFee(recipient.amount.raw.toInt()); final feeAmount = Amount( @@ -647,11 +646,7 @@ class EpiccashWallet extends Bip39Wallet { } if (info.cachedBalance.spendable == recipient.amount) { - recipient = ( - address: recipient.address, - amount: recipient.amount - feeAmount, - isChange: recipient.isChange, - ); + recipient = recipient.copyWith(amount: recipient.amount - feeAmount); } return txData.copyWith(recipients: [recipient], fee: feeAmount); diff --git a/lib/wallets/wallet/impl/litecoin_wallet.dart b/lib/wallets/wallet/impl/litecoin_wallet.dart index e72b8e633..76205c252 100644 --- a/lib/wallets/wallet/impl/litecoin_wallet.dart +++ b/lib/wallets/wallet/impl/litecoin_wallet.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../db/drift/database.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; @@ -13,9 +14,11 @@ import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../intermediate/bip39_hd_wallet.dart'; +import '../intermediate/external_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; import '../wallet_mixin_interfaces/extended_keys_interface.dart'; +import '../wallet_mixin_interfaces/mweb_interface.dart'; import '../wallet_mixin_interfaces/ordinals_interface.dart'; import '../wallet_mixin_interfaces/rbf_interface.dart'; @@ -26,7 +29,9 @@ class LitecoinWallet ExtendedKeysInterface, CoinControlInterface, RbfInterface, - OrdinalsInterface { + OrdinalsInterface, + MwebInterface + implements ExternalWallet { @override int get isarTransactionVersion => 2; @@ -51,6 +56,8 @@ class LitecoinWallet .not() .group( (q) => q + .typeEqualTo(AddressType.mweb) + .or() .typeEqualTo(AddressType.nonWallet) .or() .subTypeEqualTo(AddressSubType.nonWallet), @@ -201,30 +208,52 @@ class LitecoinWallet // Parse outputs. final List outputs = []; for (final outputJson in txData["vout"] as List) { - OutputV2 output = OutputV2.fromElectrumXJson( - Map.from(outputJson as Map), - decimalPlaces: cryptoCurrency.fractionDigits, - isFullAmountNotSats: true, - // Need addresses before we can know if the wallet owns this input. - walletOwns: false, - ); + try { + OutputV2 output = OutputV2.fromElectrumXJson( + Map.from(outputJson as Map), + decimalPlaces: cryptoCurrency.fractionDigits, + isFullAmountNotSats: true, + // Need addresses before we can know if the wallet owns this input. + walletOwns: false, + ); - // If output was to my wallet, add value to amount received. - if (receivingAddresses - .intersection(output.addresses.toSet()) - .isNotEmpty) { - wasReceivedInThisWallet = true; - amountReceivedInThisWallet += output.value; - output = output.copyWith(walletOwns: true); - } else if (changeAddresses - .intersection(output.addresses.toSet()) - .isNotEmpty) { - wasReceivedInThisWallet = true; - changeAmountReceivedInThisWallet += output.value; - output = output.copyWith(walletOwns: true); - } + // If output was to my wallet, add value to amount received. + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + amountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); + } - outputs.add(output); + outputs.add(output); + } catch (_) { + if (outputJson["ismweb"] == true) { + final outputId = outputJson["output_id"] as String; + + final db = Drift.get(walletId); + + final mwebUtxo = + await (db.select( + db.mwebUtxos, + )..where((e) => e.outputId.equals(outputId))).getSingleOrNull(); + + final output = OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "mweb", + scriptPubKeyAsm: null, + valueStringSats: mwebUtxo?.value.toString() ?? "0", + addresses: [outputId], + walletOwns: mwebUtxo != null, + ); + outputs.add(output); + } + } } final totalOut = outputs @@ -263,6 +292,10 @@ class LitecoinWallet .isNotEmpty(); if (hasOrdinal) { subType = TransactionSubType.ordinal; + } else { + if (outputs.any((e) => e.scriptPubKeyHex == "mweb")) { + subType = TransactionSubType.mweb; + } } // making API calls for every output in every transaction is too expensive diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index bb3f020f3..abf598c6f 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -5,11 +5,11 @@ import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; import 'package:namecoin/namecoin.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; @@ -578,8 +578,10 @@ class NamecoinWallet noteName += ".bit"; } + final receivingAddress = (await getCurrentReceivingAddress())!; + TxData txData = TxData( - utxos: {utxo}, + utxos: {StandardInput(utxo)}, opNameState: NameOpState( name: data.name, saltHex: data.salt, @@ -593,13 +595,14 @@ class NamecoinWallet note: "Purchase $noteName", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ - ( - address: (await getCurrentReceivingAddress())!.value, + TxRecipient( + address: receivingAddress.value, isChange: false, amount: Amount( rawValue: BigInt.from(kNameAmountSats), fractionDigits: cryptoCurrency.fractionDigits, ), + addressType: receivingAddress.type, ), ], ); @@ -627,7 +630,7 @@ class NamecoinWallet /// Builds and signs a transaction Future _createNameTx({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, required bool isForFeeCalcPurposesOnly, }) async { Logging.instance.d("Starting _createNameTx ----------"); @@ -667,19 +670,19 @@ class NamecoinWallet : 0xffffffff - 1; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (int i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + final prevOutpoint = coinlib.OutPoint(hash, inputsWithKeys[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -688,11 +691,11 @@ class NamecoinWallet final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -710,7 +713,7 @@ class NamecoinWallet case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: sequence, ); @@ -719,7 +722,7 @@ class NamecoinWallet default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -731,14 +734,14 @@ class NamecoinWallet scriptSigAsm: null, sequence: sequence, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -808,13 +811,13 @@ class NamecoinWallet try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + for (int i = 0; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( @@ -902,7 +905,7 @@ class NamecoinWallet if (customSatsPerVByte != null) { final result = await coinSelectionName( txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), - utxos: utxos?.toList(), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -940,7 +943,7 @@ class NamecoinWallet final result = await coinSelectionName( txData: txData.copyWith(feeRateAmount: rate), - utxos: utxos?.toList(), + utxos: utxos?.whereType().map((e) => e.utxo).toList(), coinControl: coinControl, ); @@ -1115,13 +1118,16 @@ class NamecoinWallet final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = + (await addSigningKeys( + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), + )).whereType().toList(); final int vSizeForOneOutput; try { vSizeForOneOutput = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1142,7 +1148,7 @@ class NamecoinWallet try { vSizeForTwoOutPuts = (await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: true, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1194,7 +1200,7 @@ class NamecoinWallet ); final txnData = await _createNameTx( isForFeeCalcPurposesOnly: false, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -1207,7 +1213,7 @@ class NamecoinWallet rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } @@ -1256,7 +1262,7 @@ class NamecoinWallet ); TxData txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1282,7 +1288,7 @@ class NamecoinWallet ); txnData = await _createNameTx( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, isForFeeCalcPurposesOnly: false, txData: txData.copyWith( recipients: await helperRecipientsConvert( @@ -1298,7 +1304,7 @@ class NamecoinWallet rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize diff --git a/lib/wallets/wallet/impl/particl_wallet.dart b/lib/wallets/wallet/impl/particl_wallet.dart index 1188625cb..08e27cc18 100644 --- a/lib/wallets/wallet/impl/particl_wallet.dart +++ b/lib/wallets/wallet/impl/particl_wallet.dart @@ -3,12 +3,12 @@ import 'dart:typed_data'; import 'package:bitcoindart/bitcoindart.dart' as bitcoindart; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/extensions/impl/uint8_list.dart'; @@ -352,7 +352,7 @@ class ParticlWallet @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required covariant List inputsWithKeys, }) async { Logging.instance.d("Starting Particl buildTransaction ----------"); @@ -371,10 +371,10 @@ class ParticlWallet ); final List<({Uint8List? output, Uint8List? redeem})> extraData = []; - for (int i = 0; i < utxoSigningData.length; i++) { - final sd = utxoSigningData[i]; + for (int i = 0; i < inputsWithKeys.length; i++) { + final sd = inputsWithKeys[i]; - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.publicKey.data; final bitcoindart.PaymentData? data; Uint8List? redeem, output; @@ -448,11 +448,11 @@ class ParticlWallet final List tempOutputs = []; // Add inputs. - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; txb.addInput( txid, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.vout, null, extraData[i].output!, cryptoCurrency.networkParams.bech32Hrp, @@ -464,14 +464,14 @@ class ParticlWallet scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), addresses: - utxoSigningData[i].utxo.address == null + inputsWithKeys[i].utxo.address == null ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -508,15 +508,15 @@ class ParticlWallet // Sign. try { - for (var i = 0; i < utxoSigningData.length; i++) { + for (var i = 0; i < inputsWithKeys.length; i++) { txb.sign( vin: i, keyPair: bitcoindart.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + inputsWithKeys[i].key!.privateKey!.data, network: convertedNetwork, - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: inputsWithKeys[i].key!.privateKey!.compressed, ), - witnessValue: utxoSigningData[i].utxo.value, + witnessValue: inputsWithKeys[i].utxo.value, redeemScript: extraData[i].redeem, overridePrefix: cryptoCurrency.networkParams.bech32Hrp, ); diff --git a/lib/wallets/wallet/impl/tezos_wallet.dart b/lib/wallets/wallet/impl/tezos_wallet.dart index 53afa8df9..2843d4a1a 100644 --- a/lib/wallets/wallet/impl/tezos_wallet.dart +++ b/lib/wallets/wallet/impl/tezos_wallet.dart @@ -251,13 +251,7 @@ class TezosWallet extends Bip39Wallet { await opList.simulate(); return txData.copyWith( - recipients: [ - ( - amount: sendAmount, - address: txData.recipients!.first.address, - isChange: txData.recipients!.first.isChange, - ), - ], + recipients: [txData.recipients!.first.copyWith(amount: sendAmount)], // fee: fee, fee: Amount( rawValue: opList.operations diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index e1c09b693..a8d5e1c65 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:compat/compat.dart' as lib_monero_compat; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; import '../../crypto_currency/crypto_currency.dart'; @@ -54,11 +55,12 @@ class WowneroWallet extends LibMoneroWallet { txData: TxData( recipients: [ // This address is only used for getting an approximate fee, never for sending - ( + TxRecipient( address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", amount: amount, isChange: false, + addressType: AddressType.cryptonote, ), ], feeRateType: feeRateType, diff --git a/lib/wallets/wallet/impl/xelis_wallet.dart b/lib/wallets/wallet/impl/xelis_wallet.dart index 2605d40ca..eea19dbdb 100644 --- a/lib/wallets/wallet/impl/xelis_wallet.dart +++ b/lib/wallets/wallet/impl/xelis_wallet.dart @@ -416,7 +416,10 @@ class XelisWallet extends LibXelisWallet { asset: xelis_sdk.xelisAsset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -477,7 +480,10 @@ class XelisWallet extends LibXelisWallet { asset: transfer.asset, ); - fee = Amount(rawValue: BigInt.zero, fractionDigits: cryptoCurrency.fractionDigits); + fee = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); outputs.add( OutputV2.isarCantDoRequiredInDefaultConstructor( @@ -719,11 +725,12 @@ class XelisWallet extends LibXelisWallet { recipients.isNotEmpty ? recipients : [ - ( + TxRecipient( address: 'xel:xz9574c80c4xegnvurazpmxhw5dlg2n0g9qm60uwgt75uqyx3pcsqzzra9m', amount: amount, isChange: false, + addressType: AddressType.xelis, ), ]; diff --git a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart index 82f3bce3c..d29e97976 100644 --- a/lib/wallets/wallet/intermediate/lib_monero_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_monero_wallet.dart @@ -11,6 +11,7 @@ import 'package:stack_wallet_backup/generate_password.dart'; import '../../../db/hive/db.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -502,9 +503,10 @@ abstract class LibMoneroWallet final host = node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; - proxy = prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; _setSyncStatus(lib_monero_compat.ConnectingSyncStatus()); try { @@ -991,7 +993,8 @@ abstract class LibMoneroWallet bool _torNodeMismatchGuard(NodeModel node) { _canPing = true; // Reset. - final bool mismatch = (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); if (mismatch) { @@ -1290,7 +1293,7 @@ abstract class LibMoneroWallet } else { final totalInputsValue = txData.utxos! .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + .fold(BigInt.zero, (p, e) => p + e); sweep = txData.amount!.raw == totalInputsValue; } @@ -1317,22 +1320,23 @@ abstract class LibMoneroWallet final height = await chainHeight; final inputs = txData.utxos - ?.map( + ?.whereType() + .map( (e) => lib_monero.Output( address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, isUnlocked: - e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, + coinbase: e.utxo.isCoinbase, ), ) .toList(); diff --git a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart index 7f7eea6f0..f18cba8d0 100644 --- a/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart +++ b/lib/wallets/wallet/intermediate/lib_salvium_wallet.dart @@ -9,6 +9,7 @@ import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; @@ -480,9 +481,10 @@ abstract class LibSalviumWallet final host = node.host.endsWith(".onion") ? node.host : Uri.parse(node.host).host; ({InternetAddress host, int port})? proxy; - proxy = prefs.useTor && !node.forceNoTor - ? TorService.sharedInstance.getProxyInfo() - : null; + proxy = + prefs.useTor && !node.forceNoTor + ? TorService.sharedInstance.getProxyInfo() + : null; _setSyncStatus(ConnectingSyncStatus()); try { @@ -956,7 +958,8 @@ abstract class LibSalviumWallet bool _torNodeMismatchGuard(NodeModel node) { _canPing = true; // Reset. - final bool mismatch = (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || + final bool mismatch = + (prefs.useTor && node.clearnetEnabled && !node.torEnabled) || (!prefs.useTor && !node.clearnetEnabled && node.torEnabled); if (mismatch) { @@ -1255,7 +1258,7 @@ abstract class LibSalviumWallet } else { final totalInputsValue = txData.utxos! .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + .fold(BigInt.zero, (p, e) => p + e); sweep = txData.amount!.raw == totalInputsValue; } @@ -1282,22 +1285,23 @@ abstract class LibSalviumWallet final height = await chainHeight; final inputs = txData.utxos - ?.map( + ?.whereType() + .map( (e) => lib_salvium.Output( - address: e.address!, - hash: e.txid, - keyImage: e.keyImage!, - value: BigInt.from(e.value), - isFrozen: e.isBlocked, + address: e.utxo.address!, + hash: e.utxo.txid, + keyImage: e.utxo.keyImage!, + value: e.value, + isFrozen: e.utxo.isBlocked, isUnlocked: - e.blockHeight != null && - (height - (e.blockHeight ?? 0)) >= + e.utxo.blockHeight != null && + (height - (e.utxo.blockHeight ?? 0)) >= cryptoCurrency.minConfirms, - height: e.blockHeight ?? 0, - vout: e.vout, - spent: e.used ?? false, + height: e.utxo.blockHeight ?? 0, + vout: e.utxo.vout, + spent: e.utxo.used ?? false, spentHeight: null, // doesn't matter here - coinbase: e.isCoinbase, + coinbase: e.utxo.isCoinbase, ), ) .toList(); diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index c2bfa5b41..554e04313 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -431,20 +431,24 @@ abstract class Wallet { hasNetwork ? NodeConnectionStatus.connected : NodeConnectionStatus.disconnected; - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent(status, walletId, cryptoCurrency), + ); + } _isConnected = hasNetwork; if (status == NodeConnectionStatus.disconnected) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } } if (hasNetwork) { @@ -511,19 +515,22 @@ abstract class Wallet { return node; } + bool doNotFireRefreshEvents = false; + // Should fire events Future refresh() async { final refreshCompleter = Completer(); final future = refreshCompleter.future.then( (_) { - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.synced, - walletId, - cryptoCurrency, - ), - ); - + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + cryptoCurrency, + ), + ); + } if (shouldAutoSync) { _periodicRefreshTimer ??= Timer.periodic(const Duration(seconds: 150), ( timer, @@ -541,20 +548,22 @@ abstract class Wallet { } }, onError: (Object e, StackTrace s) { - GlobalEventBus.instance.fire( - NodeConnectionStatusChangedEvent( - NodeConnectionStatus.disconnected, - walletId, - cryptoCurrency, - ), - ); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.unableToSync, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + cryptoCurrency, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + cryptoCurrency, + ), + ); + } Logging.instance.e( "Caught exception in refreshWalletData()", error: e, @@ -572,7 +581,11 @@ abstract class Wallet { if (this is ElectrumXInterface) { (this as ElectrumXInterface?)?.refreshingPercent = percent; } - GlobalEventBus.instance.fire(RefreshPercentChangedEvent(percent, walletId)); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(percent, walletId), + ); + } } // Should fire events @@ -593,13 +606,15 @@ abstract class Wallet { // Slight possibility of race but should be irrelevant await refreshMutex.acquire(); - GlobalEventBus.instance.fire( - WalletSyncStatusChangedEvent( - WalletSyncStatus.syncing, - walletId, - cryptoCurrency, - ), - ); + if (!doNotFireRefreshEvents) { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + cryptoCurrency, + ), + ); + } // add some small buffer before making calls. // this can probably be removed in the future but was added as a diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart index 371236180..410f11d2e 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart @@ -2,11 +2,11 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/src/utils/network.dart' as bitbox_utils; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/logger.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import '../../models/tx_data.dart'; @@ -18,7 +18,7 @@ mixin BCashInterface @override Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required covariant List inputsWithKeys, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -35,10 +35,10 @@ mixin BCashInterface final List tempOutputs = []; // Add transaction inputs - for (int i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < inputsWithKeys.length; i++) { builder.addInput( - utxoSigningData[i].utxo.txid, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.txid, + inputsWithKeys[i].utxo.vout, ); tempInputs.add( @@ -47,13 +47,14 @@ mixin BCashInterface scriptSigAsm: null, sequence: 0xffffffff - 1, outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + txid: inputsWithKeys[i].utxo.txid, + vout: inputsWithKeys[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), + addresses: + inputsWithKeys[i].utxo.address == null + ? [] + : [inputsWithKeys[i].utxo.address!], + valueStringSats: inputsWithKeys[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, coinbase: null, @@ -73,10 +74,9 @@ mixin BCashInterface 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() @@ -92,9 +92,9 @@ mixin BCashInterface try { // Sign the transaction accordingly - for (int i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < inputsWithKeys.length; i++) { final bitboxEC = bitbox.ECPair.fromPrivateKey( - utxoSigningData[i].keyPair!.privateKey.data, + inputsWithKeys[i].key!.privateKey!.data, network: bitbox_utils.Network( cryptoCurrency.networkParams.privHDPrefix, cryptoCurrency.networkParams.pubHDPrefix, @@ -103,18 +103,17 @@ mixin BCashInterface cryptoCurrency.networkParams.wifPrefix, cryptoCurrency.networkParams.p2pkhPrefix, ), - compressed: utxoSigningData[i].keyPair!.privateKey.compressed, + compressed: inputsWithKeys[i].key!.privateKey!.compressed, ); - builder.sign( - i, - bitboxEC, - utxoSigningData[i].utxo.value, - ); + builder.sign(i, bitboxEC, inputsWithKeys[i].utxo.value); } } 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/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 8fe157a61..e978a0bd7 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -4,18 +4,20 @@ import 'dart:typed_data'; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; import 'package:isar/isar.dart'; +import 'package:meta/meta.dart'; +import '../../../db/drift/database.dart'; import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/client_manager.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/coinlib/exp2pkh_address.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../models/paymint/fee_object_model.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; import '../../../utilities/enums/fee_rate_type_enum.dart'; @@ -31,6 +33,7 @@ import '../impl/firo_wallet.dart'; import '../impl/peercoin_wallet.dart'; import '../intermediate/bip39_hd_wallet.dart'; import 'cpfp_interface.dart'; +import 'mweb_interface.dart'; import 'paynym_interface.dart'; import 'rbf_interface.dart'; import 'view_only_option_interface.dart'; @@ -75,29 +78,39 @@ mixin ElectrumXInterface return false; } - Future> - helperRecipientsConvert(List addrs, List satValues) async { - final List<({String address, Amount amount, bool isChange})> results = []; + Future> helperRecipientsConvert( + List addrs, + List satValues, + ) async { + final List results = []; for (int i = 0; i < addrs.length; i++) { - results.add(( - address: addrs[i], - amount: Amount( - rawValue: satValues[i], - fractionDigits: cryptoCurrency.fractionDigits, + // assume address is valid at this point so if getAddressType fails for + // some reason default to unknown + final type = + cryptoCurrency.getAddressType(addrs[i]) ?? AddressType.unknown; + + results.add( + TxRecipient( + address: addrs[i], + amount: Amount( + rawValue: satValues[i], + fractionDigits: cryptoCurrency.fractionDigits, + ), + isChange: + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.change) + .and() + .valueEqualTo(addrs[i]) + .valueProperty() + .findFirst()) != + null, + addressType: type, ), - isChange: - (await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.change) - .and() - .valueEqualTo(addrs[i]) - .valueProperty() - .findFirst()) != - null, - )); + ); } return results; @@ -109,7 +122,8 @@ mixin ElectrumXInterface required bool isSendAll, required bool isSendAllCoinControlUtxos, int additionalOutputs = 0, - List? utxos, + List? utxos, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Starting coinSelection ----------"); @@ -120,34 +134,64 @@ mixin ElectrumXInterface throw Exception("Coin control used where utxos is null!"); } + Future
changeAddress() async { + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + return (await (this as MwebInterface).getMwebChangeAddress())!; + } else { + return (await getCurrentChangeAddress())!; + } + } + final recipientAddress = txData.recipients!.first.address; final satoshiAmountToSend = txData.amount!.raw; final int? satsPerVByte = txData.satsPerVByte; final selectedTxFeeRate = txData.feeRateAmount!; - final List availableOutputs = - utxos ?? await mainDB.getUTXOs(walletId).findAll(); + final List availableOutputs; + + if (txData.type == TxType.mweb || txData.type == TxType.mwebPegOut) { + if (utxos == null) { + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + availableOutputs = mwebUtxos.map((e) => MwebInput(e)).toList(); + } else { + availableOutputs = utxos; + } + } else { + availableOutputs = + utxos ?? + (await mainDB.getUTXOs(walletId).findAll()) + .map((e) => StandardInput(e)) + .toList(); + } + final currentChainHeight = await chainHeight; final canCPFP = this is CpfpInterface && coinControl; final spendableOutputs = - availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); + availableOutputs.where((e) { + if (e is StandardInput) { + return !e.utxo.isBlocked && + (e.utxo.used != true) && + (canCPFP || + e.utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )); + } else if (e is MwebInput) { + return !e.utxo.blocked && !e.utxo.used; + } else { + return false; + } + }).toList(); final spendableSatoshiValue = spendableOutputs.fold( BigInt.zero, - (p, e) => p + BigInt.from(e.value), + (p, e) => p + e.value, ); if (spendableSatoshiValue < satoshiAmountToSend) { @@ -180,7 +224,7 @@ mixin ElectrumXInterface BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; - final List utxoObjectsToUse = []; + final List utxoObjectsToUse = []; if (!coinControl) { for ( @@ -189,7 +233,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[i]); - satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); + satoshisBeingUsed += spendableOutputs[i].value; inputsBeingConsumed += 1; } for ( @@ -198,9 +242,7 @@ mixin ElectrumXInterface i++ ) { utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += BigInt.from( - spendableOutputs[inputsBeingConsumed].value, - ); + satoshisBeingUsed += spendableOutputs[inputsBeingConsumed].value; inputsBeingConsumed += 1; } } else { @@ -218,23 +260,37 @@ mixin ElectrumXInterface final List recipientsAmtArray = [satoshiAmountToSend]; // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = await addSigningKeys(utxoObjectsToUse); if (isSendAll || isSendAllCoinControlUtxos) { - if (satoshiAmountToSend != satoshisBeingUsed) { - throw Exception( - "Something happened that should never actually happen. " - "Please report this error to the developers.", + if ((overrideFeeAmount ?? BigInt.zero) + satoshiAmountToSend != + satoshisBeingUsed) { + Logging.instance.d("txData.type: ${txData.type}"); + Logging.instance.d("isSendAll: $isSendAll"); + Logging.instance.d( + "isSendAllCoinControlUtxos: $isSendAllCoinControlUtxos", ); + Logging.instance.d("overrideFeeAmount: $overrideFeeAmount"); + Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend"); + Logging.instance.d("satoshisBeingUsed: $satoshisBeingUsed"); + + // hack check + if (!(txData.type == TxType.mwebPegIn || + (txData.type == TxType.mweb && overrideFeeAmount != null))) { + throw Exception( + "Something happened that should never actually happen. " + "Please report this error to the developers.", + ); + } } return await _sendAllBuilder( txData: txData, recipientAddress: recipientAddress, - satoshiAmountToSend: satoshiAmountToSend, satoshisBeingUsed: satoshisBeingUsed, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, satsPerVByte: satsPerVByte, feeRatePerKB: selectedTxFeeRate, + overrideFeeAmount: overrideFeeAmount, ); } @@ -242,7 +298,7 @@ mixin ElectrumXInterface try { vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( [recipientAddress], @@ -262,10 +318,10 @@ mixin ElectrumXInterface try { vSizeForTwoOutPuts = (await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], + [recipientAddress, (await changeAddress()).value], [ satoshiAmountToSend, maxBI( @@ -282,23 +338,27 @@ mixin ElectrumXInterface } // Assume 1 output, only for recipient and no change - final feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), - ); + final feeForOneOutput = + overrideFeeAmount ?? + BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), + ); // Assume 2 outputs, one for recipient and one for change - final feeForTwoOutputs = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForTwoOutPuts) - : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), - ); + final feeForTwoOutputs = + overrideFeeAmount ?? + BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForTwoOutPuts) + : estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), + ); Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); Logging.instance.d("feeForOneOutput: $feeForOneOutput"); @@ -311,7 +371,7 @@ mixin ElectrumXInterface Logging.instance.d('Fee being paid: $difference sats'); Logging.instance.d('Estimated fee: $feeForOneOutput'); final txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, @@ -324,7 +384,7 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } @@ -354,15 +414,17 @@ mixin ElectrumXInterface // check if possible to add the change output if (changeOutputSize > cryptoCurrency.dustLimit.raw && difference - changeOutputSize == feeForTwoOutputs) { - // generate new change address if current change address has been used - await checkChangeAddressForTransactions(); - final String newChangeAddress = - (await getCurrentChangeAddress())!.value; + if (!(txData.type == TxType.mweb || + txData.type == TxType.mwebPegOut)) { + // generate new change address if current change address has been used + await checkChangeAddressForTransactions(); + } + final newChangeAddress = await changeAddress(); BigInt feeBeingPaid = difference - changeOutputSize; // add change output - recipientsArray.add(newChangeAddress); + recipientsArray.add(newChangeAddress.value); recipientsAmtArray.add(changeOutputSize); Logging.instance.d('2 outputs in tx'); @@ -373,12 +435,13 @@ mixin ElectrumXInterface Logging.instance.d('Estimated fee: $feeForTwoOutputs'); TxData txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); @@ -402,12 +465,13 @@ mixin ElectrumXInterface Logging.instance.d('Adjusted Estimated fee: $feeForTwoOutputs'); txnData = await buildTransaction( - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, txData: txData.copyWith( recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), + usedUTXOs: inputsWithKeys, ), ); } @@ -417,7 +481,7 @@ mixin ElectrumXInterface rawValue: feeBeingPaid, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize @@ -435,44 +499,52 @@ mixin ElectrumXInterface Future _sendAllBuilder({ required TxData txData, required String recipientAddress, - required BigInt satoshiAmountToSend, required BigInt satoshisBeingUsed, - required List utxoSigningData, + required List inputsWithKeys, required int? satsPerVByte, required BigInt feeRatePerKB, + BigInt? overrideFeeAmount, }) async { Logging.instance.d("Attempting to send all $cryptoCurrency"); if (txData.recipients!.length != 1) { throw Exception("Send all to more than one recipient not yet supported"); } - final int vSizeForOneOutput = - (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], + BigInt feeForOneOutput; + if (overrideFeeAmount == null) { + final int vSizeForOneOutput = + (await buildTransaction( + inputsWithKeys: inputsWithKeys, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), ), - ), - )).vSize!; - BigInt feeForOneOutput = BigInt.from( - satsPerVByte != null - ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee(vSize: vSizeForOneOutput, feeRatePerKB: feeRatePerKB), - ); + )).vSize!; + feeForOneOutput = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: feeRatePerKB, + ), + ); - if (satsPerVByte == null) { - final roughEstimate = - roughFeeEstimate(utxoSigningData.length, 1, feeRatePerKB).raw; - if (feeForOneOutput < roughEstimate) { - feeForOneOutput = roughEstimate; + if (satsPerVByte == null) { + final roughEstimate = + roughFeeEstimate(inputsWithKeys.length, 1, feeRatePerKB).raw; + if (feeForOneOutput < roughEstimate) { + feeForOneOutput = roughEstimate; + } } + } else { + feeForOneOutput = overrideFeeAmount; } - final amount = satoshiAmountToSend - feeForOneOutput; + final satoshiAmountToSend = satoshisBeingUsed - feeForOneOutput; - if (amount.isNegative) { + if (satoshiAmountToSend.isNegative) { throw Exception( "Estimated fee ($feeForOneOutput sats) is greater than balance!", ); @@ -480,9 +552,12 @@ mixin ElectrumXInterface final data = await buildTransaction( txData: txData.copyWith( - recipients: await helperRecipientsConvert([recipientAddress], [amount]), + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshiAmountToSend], + ), ), - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, ); return data.copyWith( @@ -490,29 +565,36 @@ mixin ElectrumXInterface rawValue: feeForOneOutput, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + usedUTXOs: inputsWithKeys, ); } - Future> fetchBuildTxData(List utxosToUse) async { + Future> addSigningKeys(List utxosToUse) async { // return data - final List signingData = []; + final List inputsWithKeys = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final derivePathType = cryptoCurrency.addressType( - address: utxosToUse[i].address!, - ); + final input = utxosToUse[i]; + if (input is MwebInput) { + inputsWithKeys.add(input); + } else if (input is StandardInput) { + final derivePathType = cryptoCurrency.addressType( + address: input.address!, + ); - signingData.add( - SigningData(derivePathType: derivePathType, utxo: utxosToUse[i]), - ); + inputsWithKeys.add( + StandardInput(input.utxo, derivePathType: derivePathType), + ); + } else { + throw Exception("Unknown input type ${input.runtimeType}"); + } } final root = await getRootHDNode(); - for (final sd in signingData) { + for (final sd in inputsWithKeys.whereType()) { coinlib.HDPrivateKey? keys; final address = await mainDB.getAddress(walletId, sd.utxo.address!); if (address?.derivationPath != null) { @@ -551,10 +633,10 @@ mixin ElectrumXInterface ); } - sd.keyPair = keys; + sd.key = keys; } - return signingData; + return inputsWithKeys; } catch (e, s) { Logging.instance.e("fetchBuildTxData() threw", error: e, stackTrace: s); rethrow; @@ -564,7 +646,7 @@ mixin ElectrumXInterface /// Builds and signs a transaction Future buildTransaction({ required TxData txData, - required List utxoSigningData, + required List inputsWithKeys, }) async { Logging.instance.d("Starting buildTransaction ----------"); @@ -575,7 +657,7 @@ mixin ElectrumXInterface final List prevOuts = []; coinlib.Transaction clTx = coinlib.Transaction( - version: cryptoCurrency.transactionVersion, + version: txData.type.isMweb() ? 2 : cryptoCurrency.transactionVersion, inputs: [], outputs: [], ); @@ -586,86 +668,131 @@ mixin ElectrumXInterface ? 0xffffffff - 10 : 0xffffffff - 1; + bool isMweb = false; + bool hasNonWitnessInput = false; + // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; + if (data is MwebInput) { + isMweb = true; + final address = data.address; + + final addr = await mainDB.getAddress(walletId, address); + final index = addr!.derivationIndex; + + final input = coinlib.RawInput( + prevOut: coinlib.OutPoint( + Uint8List.fromList( + data.utxo.outputId.toUint8ListFromHex.reversed.toList(), + ), + index, + ), + scriptSig: Uint8List(0), + ); - final hash = Uint8List.fromList( - txid.toUint8ListFromHex.reversed.toList(), - ); + clTx = clTx.addInput(input); - final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.outputId, + vout: index, + ), + addresses: [address], + valueStringSats: inputsWithKeys[i].value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } else if (data is StandardInput) { + final txid = data.utxo.txid; - final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), - coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, - cryptoCurrency.networkParams, - ), - ); + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); - prevOuts.add(prevOutput); + final prevOutpoint = coinlib.OutPoint(hash, data.utxo.vout); - final coinlib.Input input; + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(data.utxo.value), + coinlib.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); - switch (utxoSigningData[i].derivePathType) { - case DerivePathType.bip44: - case DerivePathType.bch44: - input = coinlib.P2PKHInput( - prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, - sequence: sequence, - ); + prevOuts.add(prevOutput); - // TODO: fix this as it is (probably) wrong! - case DerivePathType.bip49: - throw Exception("TODO p2sh"); - // input = coinlib.P2SHMultisigInput( - // prevOut: prevOutpoint, - // program: coinlib.MultisigProgram.decompile( - // utxoSigningData[i].redeemScript!, - // ), - // sequence: sequence, - // ); - - case DerivePathType.bip84: - input = coinlib.P2WPKHInput( - prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, - sequence: sequence, - ); + final coinlib.Input input; - case DerivePathType.bip86: - input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + switch (data.derivePathType) { + case DerivePathType.bip44: + case DerivePathType.bch44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - default: - throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", - ); - } + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // data.redeemScript!, + // ), + // sequence: sequence, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: data.key!.publicKey, + sequence: sequence, + ); - clTx = clTx.addInput(input); + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); - tempInputs.add( - InputV2.isarCantDoRequiredInDefaultConstructor( - scriptSigHex: input.scriptSig.toHex, - scriptSigAsm: null, - sequence: sequence, - outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( - txid: utxoSigningData[i].utxo.txid, - vout: utxoSigningData[i].utxo.vout, + default: + throw UnsupportedError( + "Unknown derivation path type found: ${data.derivePathType}", + ); + } + + if (input is! coinlib.WitnessInput) { + hasNonWitnessInput = true; + } + + clTx = clTx.addInput(input); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: data.utxo.txid, + vout: data.utxo.vout, + ), + addresses: data.utxo.address == null ? [] : [data.utxo.address!], + valueStringSats: data.utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, ), - addresses: - utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], - valueStringSats: utxoSigningData[i].utxo.value.toString(), - witness: null, - innerRedeemScriptAsm: null, - coinbase: null, - walletOwns: true, - ), - ); + ); + } else { + throw Exception("Unknown input type: ${inputsWithKeys[i].runtimeType}"); + } } // Add transaction output @@ -687,10 +814,19 @@ mixin ElectrumXInterface rethrow; } } - final output = coinlib.Output.fromAddress( - txData.recipients![i].amount.raw, - address, - ); + final coinlib.Output output; + if (address is coinlib.MwebAddress) { + isMweb = true; + output = coinlib.Output.fromProgram( + txData.recipients![i].amount.raw, + address.program, + ); + } else { + output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + } clTx = clTx.addOutput(output); @@ -712,35 +848,48 @@ mixin ElectrumXInterface ); } + if (isMweb) { + if (hasNonWitnessInput) { + throw Exception("Found non witness input in mweb tx"); + } + } + try { // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; - - if (clTx.inputs[i] is coinlib.TaprootKeyInput) { - final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, - ); - - clTx = clTx.signTaproot( - inputN: i, - key: taproot.tweakPrivateKey(key), - prevOuts: prevOuts, - ); - } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { - clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); - } else if (clTx.inputs[i] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: i, key: key); - } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { - clTx = clTx.signTaprootSingleScriptSig( - inputN: i, - key: key, - prevOuts: prevOuts, - ); + for (var i = 0; i < inputsWithKeys.length; i++) { + final data = inputsWithKeys[i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } } else { throw Exception( - "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + "Unknown input type: ${inputsWithKeys[i].runtimeType}", ); } } @@ -757,24 +906,29 @@ mixin ElectrumXInterface raw: clTx.toHex(), // dirty shortcut for peercoin's weirdness vSize: this is PeercoinWallet ? clTx.size : clTx.vSize(), - tempTx: TransactionV2( - walletId: walletId, - blockHash: null, - hash: clTx.hashHex, - txid: clTx.txid, - height: null, - timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, - inputs: List.unmodifiable(tempInputs), - outputs: List.unmodifiable(tempOutputs), - version: clTx.version, - type: - tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && - txData.paynymAccountLite == null - ? TransactionType.sentToSelf - : TransactionType.outgoing, - subType: TransactionSubType.none, - otherData: null, - ), + tempTx: + txData.type.isMweb() + ? null + : TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: + tempOutputs + .map((e) => e.walletOwns) + .fold(true, (p, e) => p &= e) && + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), ); } @@ -1655,14 +1809,30 @@ mixin ElectrumXInterface txData = txData.copyWith( usedUTXOs: - txData.usedUTXOs!.map((e) => e.copyWith(used: true)).toList(), + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), // TODO revisit setting these both txHash: txHash, txid: txHash, ); // mark utxos as used - await mainDB.putUTXOs(txData.usedUTXOs!); + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); return await updateSentCachedTxData(txData: txData); } catch (e, s) { @@ -1682,53 +1852,47 @@ mixin ElectrumXInterface throw Exception("No recipients in attempted transaction!"); } + final balance = + txData.type == TxType.mweb || txData.type == TxType.mwebPegOut + ? info.cachedBalanceSecondary + : info.cachedBalance; final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; final utxos = txData.utxos; + bool isSendAll = false; + final bool coinControl = utxos != null; final isSendAllCoinControlUtxos = coinControl && txData.amount!.raw == - utxos - .map((e) => e.value) - .fold(BigInt.zero, (p, e) => p + BigInt.from(e)); + utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + + final TxData result; if (customSatsPerVByte != null) { // check for send all - bool isSendAll = false; + isSendAll = false; if (txData.ignoreCachedBalanceChecks || - txData.amount == info.cachedBalance.spendable) { + txData.amount == balance.spendable) { isSendAll = true; } if (coinControl && this is CpfpInterface && - txData.amount == - (info.cachedBalance.spendable + - info.cachedBalance.pendingSpendable)) { + txData.amount == (balance.spendable + balance.pendingSpendable)) { isSendAll = true; } - final result = await coinSelection( + result = await coinSelection( txData: txData.copyWith(feeRateAmount: BigInt.from(-1)), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); - - Logging.instance.d("PREPARE SEND RESULT: $result"); - - if (result.fee!.raw.toInt() < result.vSize!) { - throw Exception( - "Error in fee calculation: Transaction fee cannot be less than vSize", - ); - } - - return result; } else if (feeRateType is FeeRateType || feeRateAmount is BigInt) { late final BigInt rate; if (feeRateType is FeeRateType) { @@ -1753,31 +1917,59 @@ mixin ElectrumXInterface } // check for send all - bool isSendAll = false; - if (txData.amount == info.cachedBalance.spendable) { + isSendAll = false; + if (txData.amount == balance.spendable) { isSendAll = true; } - final result = await coinSelection( + result = await coinSelection( txData: txData.copyWith(feeRateAmount: rate), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } - Logging.instance.d("prepare send: $result"); - if (result.fee!.raw.toInt() < result.vSize!) { - throw Exception( - "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " - "be less than vSize (${result.vSize})", + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})", + ); + } + + // mweb + if (result.type.isMweb()) { + final fee = await (this as MwebInterface).mwebFee(txData: result); + + TxData mwebData = await coinSelection( + txData: result.copyWith( + recipients: result.recipients!.where((e) => !(e.isChange)).toList(), + ), + utxos: utxos?.toList(), + coinControl: coinControl, + isSendAll: isSendAll, + isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, + overrideFeeAmount: fee.raw, + ); + + if (mwebData.type == TxType.mwebPegIn) { + mwebData = await buildTransaction( + txData: mwebData, + inputsWithKeys: mwebData.usedUTXOs!, ); } - - return result; - } else { - throw ArgumentError("Invalid fee rate argument provided!"); + final data = await (this as MwebInterface).processMwebTransaction( + mwebData, + ); + return data.copyWith(fee: fee); } + + Logging.instance.d("prepare send: $result"); + + return result; } catch (e, s) { Logging.instance.e( "Exception rethrown from prepareSend(): ", @@ -1788,6 +1980,7 @@ mixin ElectrumXInterface } } + @mustCallSuper @override Future init() async { try { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart new file mode 100644 index 000000000..a870aaa1e --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/mweb_interface.dart @@ -0,0 +1,973 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as cl; +import 'package:drift/drift.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:isar/isar.dart'; +import 'package:mweb_client/mweb_client.dart'; + +import '../../../db/drift/database.dart'; +import '../../../models/balance.dart'; +import '../../../models/input.dart'; +import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; +import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/isar/models/isar_models.dart'; +import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; +import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; +import '../../../services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import '../../../services/event_bus/global_event_bus.dart'; +import '../../../services/mwebd_service.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; +import '../intermediate/external_wallet.dart'; +import 'electrumx_interface.dart'; + +mixin MwebInterface + on ElectrumXInterface + implements ExternalWallet { + StreamSubscription? _mwebUtxoSubscription; + + Future get _scanSecret async => + (await getRootHDNode()).derivePath("m/1000'/0'").privateKey.data; + Future get _spendSecret async => + (await getRootHDNode()).derivePath("m/1000'/1'").privateKey.data; + Future get _spendPub async => + (await getRootHDNode()).derivePath("m/1000'/1'").publicKey.data; + + Future getCurrentReceivingMwebAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); + } + + Future getMwebChangeAddress() async { + return await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.mweb) + .and() + .subTypeEqualTo(AddressSubType.change) + .and() + .derivationIndexEqualTo(0) + .findFirst(); + } + + Future get _client async { + final client = await MwebdService.instance.getClient( + cryptoCurrency.network, + ); + if (client == null) { + throw Exception("Fetched mweb client returned null"); + } + return client; + } + + WalletSyncStatus? _syncStatusMwebCache; + WalletSyncStatus? get _syncStatusMweb => _syncStatusMwebCache; + set _syncStatusMweb(WalletSyncStatus? newValue) { + switch (newValue) { + case null: + doNotFireRefreshEvents = true; + case WalletSyncStatus.unableToSync: + doNotFireRefreshEvents = true; + case WalletSyncStatus.synced: + doNotFireRefreshEvents = false; + case WalletSyncStatus.syncing: + doNotFireRefreshEvents = true; + } + + _syncStatusMwebCache = newValue; + } + + Timer? _mwebdPolling; + int currentKnownChainHeight = 0; + double highestPercentCached = 0; + void _startPollingMwebd() async { + _mwebdPolling?.cancel(); + _mwebdPolling = Timer.periodic(const Duration(seconds: 5), (_) async { + try { + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + + Logging.instance.t( + "$walletId ${info.name} _polling mwebd status: $status", + ); + + if (status == null) { + throw Exception( + "Mwebd server status is null. Was mwebd initialized?", + ); + } + + final currentKnownChainHeight = await chainHeight; + + final ({int remaining, double percent})? syncInfo; + + if (status.blockHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.blockHeaderHeight, + percent: status.blockHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebHeaderHeight < currentKnownChainHeight) { + syncInfo = ( + remaining: currentKnownChainHeight - status.mwebHeaderHeight, + percent: status.mwebHeaderHeight / currentKnownChainHeight, + ); + } else if (status.mwebUtxosHeight < currentKnownChainHeight) { + syncInfo = (remaining: 1, percent: 0.99); + } else { + syncInfo = null; + } + + WalletSyncStatus? syncStatus; + + if (syncInfo != null) { + final previous = highestPercentCached; + highestPercentCached = math.max( + highestPercentCached, + syncInfo.percent, + ); + + if (previous != highestPercentCached) { + GlobalEventBus.instance.fire( + RefreshPercentChangedEvent(highestPercentCached, walletId), + ); + GlobalEventBus.instance.fire( + BlocksRemainingEvent(syncInfo.remaining, walletId), + ); + } + + syncStatus = WalletSyncStatus.syncing; + } else { + syncStatus = WalletSyncStatus.synced; + } + + _syncStatusMweb = syncStatus; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(syncStatus, walletId, info.coin), + ); + } catch (e, s) { + Logging.instance.e( + "mweb wallet polling error", + error: e, + stackTrace: s, + ); + _syncStatusMweb = WalletSyncStatus.unableToSync; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent(_syncStatusMweb!, walletId, info.coin), + ); + } + }); + } + + Future _stopUpdateMwebUtxos() async => + await _mwebUtxoSubscription?.cancel(); + + Future _startUpdateMwebUtxos() async { + await _stopUpdateMwebUtxos(); + + final client = await _client; + + Logging.instance.i("info.restoreHeight: ${info.restoreHeight}"); + Logging.instance.i( + "info.otherData[WalletInfoKeys.mwebScanHeight]: ${info.otherData[WalletInfoKeys.mwebScanHeight]}", + ); + final fromHeight = + info.otherData[WalletInfoKeys.mwebScanHeight] as int? ?? + info.restoreHeight; + + final request = UtxosRequest( + fromHeight: fromHeight, + scanSecret: await _scanSecret, + ); + + final db = Drift.get(walletId); + _mwebUtxoSubscription = (await client.utxos(request)).listen((utxo) async { + Logging.instance.t( + "Found UTXO in stream: Utxo(" + "height: ${utxo.height}, " + "value: ${utxo.value}, " + "address: ${utxo.address}, " + "outputId: ${utxo.outputId}, " + "blockTime: ${utxo.blockTime}" + ")", + ); + + if (utxo.address.isNotEmpty && utxo.outputId.isNotEmpty) { + try { + await db.transaction(() async { + final prev = + await (db.select(db.mwebUtxos)..where( + (e) => e.outputId.equals(utxo.outputId), + )).getSingleOrNull(); + + final newUtxo = MwebUtxosCompanion( + outputId: Value(prev?.outputId ?? utxo.outputId), + address: Value(prev?.address ?? utxo.address), + value: Value(utxo.value.toInt()), + height: Value(utxo.height), + blockTime: Value(utxo.blockTime), + blocked: Value(prev?.blocked ?? false), + used: Value(prev?.used ?? false), + ); + + await db.into(db.mwebUtxos).insertOnConflictUpdate(newUtxo); + }); + + Address? addr = await mainDB.getAddress(walletId, utxo.address); + while (addr == null || addr.value != utxo.address) { + addr = await generateNextMwebAddress(); + await mainDB.updateOrPutAddresses([addr]); + } + + // TODO get real txid one day + final fakeTxid = "mweb_outputId_${utxo.outputId}"; + + final tx = TransactionV2( + walletId: walletId, + blockHash: null, // ?? + hash: "", + txid: fakeTxid, + timestamp: + utxo.height < 1 + ? DateTime.now().millisecondsSinceEpoch ~/ 1000 + : utxo.blockTime, + height: utxo.height, + inputs: [], + outputs: [ + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "", + valueStringSats: utxo.value.toString(), + addresses: [utxo.address], + walletOwns: true, + ), + ], + version: 2, // probably + type: TransactionType.incoming, + subType: TransactionSubType.mweb, + otherData: jsonEncode({ + TxV2OdKeys.overrideFee: + Amount( + rawValue: + BigInt + .zero, // TODO fill in correctly when we have a real txid + fractionDigits: cryptoCurrency.fractionDigits, + ).toJsonString(), + }), + ); + + await mainDB.updateOrPutTransactionV2s([tx]); + + await updateBalance(); + + if (utxo.height > fromHeight) { + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: utxo.height}, + isar: mainDB.isar, + ); + } + } catch (e, s) { + Logging.instance.f( + "Failed to insert/update mweb utxo", + error: e, + stackTrace: s, + ); + } + } else { + Logging.instance.w("Empty mweb utxo not added to db... ??"); + } + }); + } + + Future _initMweb() async { + try { + // check server is up + final status = await MwebdService.instance.getServerStatus( + cryptoCurrency.network, + ); + if (status == null) { + await MwebdService.instance.initService(cryptoCurrency.network); + } + + _startPollingMwebd(); + } catch (e, s) { + Logging.instance.e("testing initMweb failed", error: e, stackTrace: s); + } + } + + /// [isChange] will always return the change address at index 0 !!!!! + Future
generateNextMwebAddress({bool isChange = false}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling generateNextMwebAddress with mweb disabled for $walletId ${info.name}", + ); + } + + final int nextIndex; + if (isChange) { + nextIndex = 0; + } else { + final highestStoredIndex = + (await getCurrentReceivingMwebAddress())?.derivationIndex ?? 0; + + nextIndex = highestStoredIndex + 1; + } + + final client = await _client; + + final response = await client.address( + await _scanSecret, + await _spendPub, + nextIndex, + ); + + return Address( + walletId: walletId, + value: response, + publicKey: [], + derivationIndex: nextIndex, + derivationPath: null, + type: AddressType.mweb, + subType: isChange ? AddressSubType.change : AddressSubType.receiving, + ); + } + + Future processMwebTransaction(TxData txData) async { + final client = await _client; + final response = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: false, + ), + ); + + if (txData.type == TxType.mwebPegIn) { + cl.Transaction clTx = cl.Transaction.fromBytes( + Uint8List.fromList(response.rawTx), + ); + + assert(response.rawTx.toString() == clTx.toBytes().toList().toString()); + final List prevOuts = []; + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + if (data is StandardInput) { + final prevOutput = cl.Output.fromAddress( + BigInt.from(data.utxo.value), + cl.Address.fromString( + data.utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + } + } + + for (int i = 0; i < txData.usedUTXOs!.length; i++) { + final data = txData.usedUTXOs![i]; + + if (data is MwebInput) { + // do nothing + } else if (data is StandardInput) { + final value = BigInt.from(data.utxo.value); + final key = data.key!.privateKey!; + if (clTx.inputs[i] is cl.TaprootKeyInput) { + final taproot = cl.Taproot(internalKey: data.key!.publicKey); + + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is cl.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is cl.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is cl.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); + } + } else { + throw Exception("Unknown input type: ${data.runtimeType}"); + } + } + return txData.copyWith(raw: clTx.toHex()); + } else { + return txData.copyWith(raw: Uint8List.fromList(response.rawTx).toHex); + } + } + + Future _confirmSendMweb({required TxData txData}) async { + if (!info.isMwebEnabled) { + throw Exception( + "Tried calling _confirmSendMweb with mweb disabled for $walletId ${info.name}", + ); + } + + try { + Logging.instance.d("_confirmSendMweb txData: $txData"); + + final client = await _client; + + final response = await client.broadcast( + BroadcastRequest(rawTx: txData.raw!.toUint8ListFromHex), + ); + + final txHash = response.txid; + Logging.instance.d("Sent txHash: $txHash"); + + txData = txData.copyWith( + usedUTXOs: + txData.usedUTXOs!.map((e) { + if (e is StandardInput) { + return StandardInput( + e.utxo.copyWith(used: true), + derivePathType: e.derivePathType, + ); + } else if (e is MwebInput) { + return MwebInput(e.utxo.copyWith(used: true)); + } else { + return e; + } + }).toList(), + txHash: txHash, + txid: txHash, + ); + + // mark utxos as used + await mainDB.putUTXOs( + txData.usedUTXOs! + .whereType() + .map((e) => e.utxo) + .toList(), + ); + + // Update used mweb utxos as used in database + await _checkSpentMwebUtxos(); + + return await updateSentCachedTxData(txData: txData); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from _confirmSendMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future prepareSend({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + if (hasMwebOutputs) { + // assume pegin tx + txData = txData.copyWith(type: TxType.mwebPegIn); + } + + return super.prepareSend(txData: txData); + } + + /// prepare mweb transaction where spending mweb outputs + Future prepareSendMweb({required TxData txData}) async { + final hasMwebOutputs = + txData.recipients! + .where((e) => e.addressType == AddressType.mweb) + .isNotEmpty; + + final type = hasMwebOutputs ? TxType.mweb : TxType.mwebPegOut; + + txData = txData.copyWith(type: type); + + return super.prepareSend(txData: txData); + } + + Future anonymizeAllMweb() async { + if (!info.isMwebEnabled) { + Logging.instance.e( + "Tried calling anonymizeAllMweb with mweb disabled for $walletId ${info.name}", + ); + return; + } + + try { + 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(); + + spendableUtxos.removeWhere( + (e) => + !e.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), + ); + + if (spendableUtxos.isEmpty) { + throw Exception("No available UTXOs found to anonymize"); + } + + final amount = spendableUtxos.fold( + Amount.zeroWith(fractionDigits: cryptoCurrency.fractionDigits), + (p, e) => + p + + Amount( + rawValue: BigInt.from(e.value), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ); + + // TODO finish + final txData = await prepareSend( + txData: TxData( + type: TxType.mwebPegIn, + feeRateType: FeeRateType.average, + recipients: [ + TxRecipient( + address: (await getCurrentReceivingMwebAddress())!.value, + amount: amount, + isChange: false, + addressType: AddressType.mweb, + ), + ], + ), + ); + + await _confirmSendMweb(txData: txData); + } catch (e, s) { + Logging.instance.w( + "Exception caught in anonymizeAllMweb(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + Future _checkSpentMwebUtxos() async { + try { + final db = Drift.get(walletId); + final mwebUtxos = await db.select(db.mwebUtxos).get(); + + final client = await _client; + + final spent = await client.spent( + SpentRequest(outputId: mwebUtxos.map((e) => e.outputId)), + ); + + await db.transaction(() async { + for (final utxo in mwebUtxos) { + await db + .into(db.mwebUtxos) + .insertOnConflictUpdate( + utxo + .toCompanion(false) + .copyWith( + used: Value(spent.outputId.contains(utxo.outputId)), + ), + ); + } + }); + } catch (e, s) { + Logging.instance.e("_checkSpentMwebUtxos()", error: e, stackTrace: s); + } + } + + Future _checkAddresses() async { + // check change first as it is index 0 + Address? changeAddress = await getMwebChangeAddress(); + if (changeAddress == null) { + changeAddress = await generateNextMwebAddress(isChange: true); + await mainDB.putAddress(changeAddress); + } + + // check recieving + Address? address = await getCurrentReceivingMwebAddress(); + if (address == null) { + address = await generateNextMwebAddress(); + await mainDB.putAddress(address); + } + } + + // =========================================================================== + + @override + Future confirmSend({required TxData txData}) async { + if (txData.type.isMweb()) { + return await _confirmSendMweb(txData: txData); + } else { + return await super.confirmSend(txData: txData); + } + } + + @override + Future open() async { + if (info.isMwebEnabled) { + try { + await _initMweb(); + + await _checkAddresses(); + + unawaited(_startUpdateMwebUtxos()); + } catch (e, s) { + // do nothing, still allow user into wallet + Logging.instance.e( + "$runtimeType init() failed", + error: e, + stackTrace: s, + ); + } + } + } + + @override + Future updateBalance() async { + // call to super to update transparent balance + final normalBalanceFuture = super.updateBalance(); + + if (info.isMwebEnabled) { + final start = DateTime.now(); + try { + await _checkSpentMwebUtxos(); + + final currentHeight = await chainHeight; + final db = Drift.get(walletId); + final mwebUtxos = + await (db.select(db.mwebUtxos) + ..where((e) => e.used.equals(false))).get(); + + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: cryptoCurrency.fractionDigits, + ); + + for (final utxo in mwebUtxos) { + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: cryptoCurrency.fractionDigits, + ); + + satoshiBalanceTotal += utxoAmount; + + if (utxo.blocked) { + satoshiBalanceBlocked += utxoAmount; + } else { + if (utxo.isConfirmed( + currentHeight, + cryptoCurrency.minConfirms, + // overrideMinConfirms: TODO: set this??? + )) { + satoshiBalanceSpendable += utxoAmount; + } else { + satoshiBalancePending += utxoAmount; + } + } + } + + final balance = Balance( + total: satoshiBalanceTotal, + spendable: satoshiBalanceSpendable, + blockedTotal: satoshiBalanceBlocked, + pendingSpendable: satoshiBalancePending, + ); + + await info.updateBalanceSecondary( + newBalance: balance, + isar: mainDB.isar, + ); + } catch (e, s) { + Logging.instance.e( + "$runtimeType updateBalance mweb $walletId ${info.name}: ", + error: e, + stackTrace: s, + ); + } finally { + Logging.instance.d( + "${info.name} updateBalance mweb duration:" + " ${DateTime.now().difference(start)}", + ); + } + } + + // wait for normalBalanceFuture to complete before returning + await normalBalanceFuture; + } + + @override + Future recover({required bool isRescan}) async { + if (isViewOnly) { + await recoverViewOnly(isRescan: isRescan); + return; + } + + final start = DateTime.now(); + final root = await getRootHDNode(); + + final List addresses})>> receiveFutures = + []; + final List addresses})>> changeFutures = + []; + + const receiveChain = 0; + const changeChain = 1; + + const txCountBatchSize = 12; + + try { + await refreshMutex.protect(() async { + if (isRescan) { + await _stopUpdateMwebUtxos(); + + // clear cache + await electrumXCachedClient.clearSharedTransactionCache( + cryptoCurrency: info.coin, + ); + // clear blockchain info + await mainDB.deleteWalletBlockchainData(walletId); + + // reset scan/listen height + await info.updateOtherData( + newEntries: {WalletInfoKeys.mwebScanHeight: info.restoreHeight}, + isar: mainDB.isar, + ); + + // reset balance to 0 + await info.updateBalanceSecondary( + newBalance: Balance.zeroFor(currency: cryptoCurrency), + isar: mainDB.isar, + ); + + // clear all mweb utxos + final db = Drift.get(walletId); + await db.transaction(() async => await db.delete(db.mwebUtxos).go()); + + if (info.isMwebEnabled) { + await _checkAddresses(); + + // only restart scanning if mweb enabled + unawaited(_startUpdateMwebUtxos()); + } + } + + // receiving addresses + Logging.instance.i("checking receiving addresses..."); + + final canBatch = await serverCanBatch; + + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + receiveFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), + ); + } + + // change addresses + Logging.instance.d("checking change addresses..."); + for (final type in cryptoCurrency.supportedDerivationPathTypes) { + changeFutures.add( + canBatch + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), + ); + } + + // io limitations may require running these linearly instead + final futuresResult = await Future.wait([ + Future.wait(receiveFutures), + Future.wait(changeFutures), + ]); + + final receiveResults = futuresResult[0]; + final changeResults = futuresResult[1]; + + final List
addressesToStore = []; + + int highestReceivingIndexWithHistory = 0; + + for (final tuple in receiveResults) { + if (tuple.addresses.isEmpty) { + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } + } else { + highestReceivingIndexWithHistory = math.max( + tuple.index, + highestReceivingIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + int highestChangeIndexWithHistory = 0; + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + for (final tuple in changeResults) { + if (tuple.addresses.isEmpty) { + await checkChangeAddressForTransactions(); + } else { + highestChangeIndexWithHistory = math.max( + tuple.index, + highestChangeIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + // remove extra addresses to help minimize risk of creating a large gap + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.change && + e.derivationIndex > highestChangeIndexWithHistory, + ); + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.receiving && + e.derivationIndex > highestReceivingIndexWithHistory, + ); + + await mainDB.updateOrPutAddresses(addressesToStore); + }); + + unawaited(refresh()); + Logging.instance.i( + "Mweb recover for " + "${info.name}: ${DateTime.now().difference(start)}", + ); + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from mweb_interface recover(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + @override + Future exit() async { + _mwebdPolling?.cancel(); + _mwebdPolling = null; + await super.exit(); + } + + bool isMwebAddress(String address) { + try { + cl.MwebAddress.fromString(address, network: cryptoCurrency.networkParams); + return true; + } catch (_) { + return false; + } + } + + Future mwebFee({required TxData txData}) async { + final outputs = txData.recipients!; + final utxos = txData.usedUTXOs!; + + final sumOfUtxosValue = utxos.fold(BigInt.zero, (p, e) => p + e.value); + + final preOutputSum = outputs.fold(BigInt.zero, (p, e) => p + e.amount.raw); + final fee = sumOfUtxosValue - preOutputSum; + + final client = await _client; + + final resp = await client.create( + CreateRequest( + rawTx: txData.raw!.toUint8ListFromHex, + scanSecret: await _scanSecret, + spendSecret: await _spendSecret, + feeRatePerKb: Int64(txData.feeRateAmount!.toInt()), + dryRun: true, + ), + ); + + final processedTx = cl.Transaction.fromBytes( + Uint8List.fromList(resp.rawTx), + ); + + BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; + final posUtxos = + utxos + .where( + (utxo) => processedTx.inputs.any( + (input) => + input.prevOut.hash.toHex == + Uint8List.fromList( + utxo.id.toUint8ListFromHex.reversed.toList(), + ).toHex, + ), + ) + .toList(); + + final posOutputSum = processedTx.outputs.fold( + BigInt.zero, + (acc, output) => acc + output.value, + ); + final mwebInputSum = + sumOfUtxosValue - posUtxos.fold(BigInt.zero, (p, e) => p + e.value); + final expectedPegin = maxBI(BigInt.zero, (preOutputSum - mwebInputSum)); + BigInt feeIncrease = posOutputSum - expectedPegin; + + if (expectedPegin > BigInt.zero) { + feeIncrease += BigInt.from( + (txData.feeRateAmount! / BigInt.from(1000) * 41).ceil(), + ); + } + + return Amount( + rawValue: fee + feeIncrease, + fractionDigits: cryptoCurrency.fractionDigits, + ); + } +} diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 1bfb3a2a3..3ad0425d5 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -14,11 +14,11 @@ import 'package:tuple/tuple.dart'; import '../../../exceptions/wallet/insufficient_balance_exception.dart'; import '../../../exceptions/wallet/paynym_send_exception.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; -import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/bip32_utils.dart'; import '../../../utilities/bip47_utils.dart'; @@ -379,10 +379,11 @@ mixin PaynymInterface return prepareSend( txData: txData.copyWith( recipients: [ - ( + TxRecipient( address: sendToAddress.value, amount: txData.recipients!.first.amount, isChange: false, + addressType: sendToAddress.type, ), ], ), @@ -526,12 +527,15 @@ mixin PaynymInterface } // gather required signing data - final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + final inputsWithKeys = + (await addSigningKeys( + utxoObjectsToUse.map((e) => StandardInput(e)).toList(), + )).whereType().toList(); final vSizeForNoChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, // override amount to get around absurd fees error overrideAmountForTesting: satoshisBeingUsed, @@ -541,7 +545,7 @@ mixin PaynymInterface final vSizeForWithChange = BigInt.from( (await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: satoshisBeingUsed - amountToSend.raw, )).item2, ); @@ -584,7 +588,7 @@ mixin PaynymInterface feeForWithChange) { var txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); @@ -597,7 +601,7 @@ mixin PaynymInterface feeBeingPaid += BigInt.one; txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: changeAmount, ); } @@ -605,10 +609,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -616,7 +621,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -626,7 +631,7 @@ mixin PaynymInterface // than the dust limit. Try without change final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -635,10 +640,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -646,7 +652,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -657,7 +663,7 @@ mixin PaynymInterface // build without change here final txn = await _createNotificationTx( targetPaymentCodeString: targetPaymentCodeString, - utxoSigningData: utxoSigningData, + inputsWithKeys: inputsWithKeys, change: BigInt.zero, ); @@ -666,10 +672,11 @@ mixin PaynymInterface final txData = TxData( raw: txn.item1, recipients: [ - ( + TxRecipient( address: targetPaymentCodeString, amount: amountToSend, isChange: false, + addressType: AddressType.unknown, ), ], fee: Amount( @@ -677,7 +684,7 @@ mixin PaynymInterface fractionDigits: cryptoCurrency.fractionDigits, ), vSize: txn.item2, - utxos: utxoSigningData.map((e) => e.utxo).toSet(), + utxos: inputsWithKeys.toSet(), note: "PayNym connect", ); @@ -706,7 +713,7 @@ mixin PaynymInterface // equal to its vSize Future> _createNotificationTx({ required String targetPaymentCodeString, - required List utxoSigningData, + required List inputsWithKeys, required BigInt change, BigInt? overrideAmountForTesting, }) async { @@ -717,7 +724,7 @@ mixin PaynymInterface ); final myCode = await getPaymentCode(isSegwit: false); - final utxo = utxoSigningData.first.utxo; + final utxo = inputsWithKeys.first.utxo; final txPoint = utxo.txid.toUint8ListFromHex.reversed.toList(); final txPointIndex = utxo.vout; @@ -726,10 +733,10 @@ mixin PaynymInterface final buffer = rev.buffer.asByteData(); buffer.setUint32(txPoint.length, txPointIndex, Endian.little); - final myKeyPair = utxoSigningData.first.keyPair!; + final myKeyPair = inputsWithKeys.first.key!; final S = SecretPoint( - myKeyPair.privateKey.data, + myKeyPair.privateKey!.data, targetPaymentCode.notificationPublicKey(), ); @@ -756,8 +763,8 @@ mixin PaynymInterface outputs: [], ); - for (var i = 0; i < utxoSigningData.length; i++) { - final txid = utxoSigningData[i].utxo.txid; + for (var i = 0; i < inputsWithKeys.length; i++) { + final txid = inputsWithKeys[i].utxo.txid; final hash = Uint8List.fromList( txid.toUint8ListFromHex.reversed.toList(), @@ -765,13 +772,13 @@ mixin PaynymInterface final prevOutpoint = coinlib.OutPoint( hash, - utxoSigningData[i].utxo.vout, + inputsWithKeys[i].utxo.vout, ); final prevOutput = coinlib.Output.fromAddress( - BigInt.from(utxoSigningData[i].utxo.value), + BigInt.from(inputsWithKeys[i].utxo.value), coinlib.Address.fromString( - utxoSigningData[i].utxo.address!, + inputsWithKeys[i].utxo.address!, cryptoCurrency.networkParams, ), ); @@ -780,12 +787,12 @@ mixin PaynymInterface final coinlib.Input input; - switch (utxoSigningData[i].derivePathType) { + switch (inputsWithKeys[i].derivePathType) { case DerivePathType.bip44: case DerivePathType.bch44: input = coinlib.P2PKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -803,7 +810,7 @@ mixin PaynymInterface case DerivePathType.bip84: input = coinlib.P2WPKHInput( prevOut: prevOutpoint, - publicKey: utxoSigningData[i].keyPair!.publicKey, + publicKey: inputsWithKeys[i].key!.publicKey, sequence: 0xffffffff - 1, ); @@ -812,7 +819,7 @@ mixin PaynymInterface default: throw UnsupportedError( - "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + "Unknown derivation path type found: ${inputsWithKeys[i].derivePathType}", ); } @@ -860,21 +867,21 @@ mixin PaynymInterface clTx = clTx.signTaproot( inputN: 0, - key: taproot.tweakPrivateKey(myKeyPair.privateKey), + key: taproot.tweakPrivateKey(myKeyPair.privateKey!), prevOuts: prevOuts, ); } else if (clTx.inputs[0] is coinlib.LegacyWitnessInput) { clTx = clTx.signLegacyWitness( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, value: BigInt.from(utxo.value), ); } else if (clTx.inputs[0] is coinlib.LegacyInput) { - clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey); + clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey!); } else if (clTx.inputs[0] is coinlib.TaprootSingleScriptSigInput) { clTx = clTx.signTaprootSingleScriptSig( inputN: 0, - key: myKeyPair.privateKey, + key: myKeyPair.privateKey!, prevOuts: prevOuts, ); } else { @@ -884,13 +891,13 @@ mixin PaynymInterface } // sign rest of possible inputs - for (int i = 1; i < utxoSigningData.length; i++) { - final value = BigInt.from(utxoSigningData[i].utxo.value); - final key = utxoSigningData[i].keyPair!.privateKey; + for (int i = 1; i < inputsWithKeys.length; i++) { + final value = BigInt.from(inputsWithKeys[i].utxo.value); + final key = inputsWithKeys[i].key!.privateKey!; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( - internalKey: utxoSigningData[i].keyPair!.publicKey, + internalKey: inputsWithKeys[i].key!.publicKey, ); clTx = clTx.signTaproot( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart index 2e609aeb0..03a8b6dd6 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:isar/isar.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../utilities/amount/amount.dart'; @@ -46,14 +47,15 @@ mixin RbfInterface required TransactionV2 oldTransaction, required int newRate, }) async { - final note = await mainDB.isar.transactionNotes - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(oldTransaction.txid) - .findFirst(); + final note = + await mainDB.isar.transactionNotes + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(oldTransaction.txid) + .findFirst(); - final Set utxos = {}; + final Set utxos = {}; for (final input in oldTransaction.inputs) { final utxo = UTXO( walletId: walletId, @@ -71,7 +73,7 @@ mixin RbfInterface address: input.addresses.first, ); - utxos.add(utxo); + utxos.add(StandardInput(utxo)); } final List recipients = []; @@ -86,43 +88,44 @@ mixin RbfInterface final isChange = addressModel?.subType == AddressSubType.change; recipients.add( - ( + TxRecipient( address: address, amount: Amount( - rawValue: output.value, - fractionDigits: cryptoCurrency.fractionDigits), + rawValue: output.value, + fractionDigits: cryptoCurrency.fractionDigits, + ), isChange: isChange, + addressType: cryptoCurrency.getAddressType(address)!, ), ); } - final oldFee = oldTransaction - .getFee(fractionDigits: cryptoCurrency.fractionDigits) - .raw; - final inSum = utxos - .map((e) => BigInt.from(e.value)) - .fold(BigInt.zero, (p, e) => p + e); + final oldFee = + oldTransaction + .getFee(fractionDigits: cryptoCurrency.fractionDigits) + .raw; + final inSum = utxos.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); final noChange = recipients.map((e) => e.isChange).fold(false, (p, e) => p || e) == - false; - final otherAvailableUtxos = await mainDB - .getUTXOs(walletId) - .filter() - .isBlockedEqualTo(false) - .and() - .group( - (q) => q.usedIsNull().or().usedEqualTo(false), - ) - .findAll(); + false; + final otherAvailableUtxos = + await mainDB + .getUTXOs(walletId) + .filter() + .isBlockedEqualTo(false) + .and() + .group((q) => q.usedIsNull().or().usedEqualTo(false)) + .findAll(); final height = await chainHeight; otherAvailableUtxos.removeWhere( - (e) => !e.isConfirmed( - height, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ), + (e) => + !e.isConfirmed( + height, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ), ); TxData txData = TxData( @@ -138,13 +141,14 @@ mixin RbfInterface // safe to assume send all? txData = txData.copyWith( recipients: [ - ( + TxRecipient( address: recipients.first.address, amount: Amount( rawValue: inSum, fractionDigits: cryptoCurrency.fractionDigits, ), isChange: false, + addressType: recipients.first.addressType, ), ], ); @@ -159,8 +163,9 @@ mixin RbfInterface throw Exception("New fee in RBF has not changed at all"); } - final indexOfChangeOutput = - txData.recipients!.indexWhere((e) => e.isChange); + final indexOfChangeOutput = txData.recipients!.indexWhere( + (e) => e.isChange, + ); final removed = txData.recipients!.removeAt(indexOfChangeOutput); @@ -172,13 +177,11 @@ mixin RbfInterface // update recipients txData.recipients!.insert( indexOfChangeOutput, - ( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); Logging.instance.d( @@ -206,7 +209,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(txData.utxos!.toList()), + inputsWithKeys: await addSigningKeys(txData.utxos!.toList()), ); // if change amount is negative @@ -232,19 +235,17 @@ mixin RbfInterface } txData.recipients!.insert( indexOfChangeOutput, - ( - address: removed.address, + removed.copyWith( amount: Amount( rawValue: newChangeAmount, fractionDigits: cryptoCurrency.fractionDigits, ), - isChange: removed.isChange, ), ); final newUtxoSet = { - ...txData.utxos!, - ...extraUtxos, + ...txData.utxos!.whereType(), + ...extraUtxos.map((e) => StandardInput(e)), }; // TODO: remove assert @@ -264,7 +265,7 @@ mixin RbfInterface fractionDigits: cryptoCurrency.fractionDigits, ), ), - utxoSigningData: await fetchBuildTxData(newUtxoSet.toList()), + inputsWithKeys: await addSigningKeys(newUtxoSet.toList()), ); } } else { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 21bd7a74a..d5d5e756d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -15,11 +15,11 @@ import 'package:logger/logger.dart'; import '../../../db/drift/database.dart' show Drift; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/balance.dart'; +import '../../../models/input.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; 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'; @@ -480,8 +480,7 @@ mixin SparkInterface txb.setLockTime(await chainHeight); txb.setVersion(3 | (9 << 16)); - List<({String address, Amount amount, bool isChange})>? - recipientsWithFeeSubtracted; + List? recipientsWithFeeSubtracted; List<({String address, Amount amount, String memo, bool isChange})>? sparkRecipientsWithFeeSubtracted; final recipientCount = @@ -535,16 +534,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, + recipientsWithFeeSubtracted!.add( + txData.recipients![i].copyWith( + 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, @@ -1413,7 +1412,7 @@ mixin SparkInterface ? max(0, currentHeight - random.nextInt(100)) : currentHeight; const txVersion = 1; - final List vin = []; + final List vin = []; final List<(dynamic, int, String?)> vout = []; BigInt nFeeRet = BigInt.zero; @@ -1426,7 +1425,7 @@ mixin SparkInterface } BigInt nValueToSelect, mintedValue; - final List setCoins = []; + final List setCoins = []; bool skipCoin = false; // Start with no fee and loop until there is enough fee @@ -1555,7 +1554,11 @@ mixin SparkInterface BigInt nValueIn = BigInt.zero; for (final utxo in itr) { if (nValueToSelect > nValueIn) { - setCoins.add((await fetchBuildTxData([utxo])).first); + setCoins.add( + (await addSigningKeys([ + StandardInput(utxo), + ])).whereType().first, + ); nValueIn += BigInt.from(utxo.value); } } @@ -1595,7 +1598,7 @@ mixin SparkInterface for (final sd in setCoins) { vin.add(sd); - final pubKey = sd.keyPair!.publicKey.data; + final pubKey = sd.key!.publicKey.data; final btc.PaymentData? data; switch (sd.derivePathType) { @@ -1659,9 +1662,9 @@ mixin SparkInterface dummyTxb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - setCoins[i].keyPair!.privateKey.data, + setCoins[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: setCoins[i].keyPair!.privateKey.compressed, + compressed: setCoins[i].key!.privateKey!.compressed, ), witnessValue: setCoins[i].utxo.value, @@ -1756,7 +1759,7 @@ mixin SparkInterface txb.setVersion(txVersion); txb.setLockTime(lockTime); for (final input in vin) { - final pubKey = input.keyPair!.publicKey.data; + final pubKey = input.key!.publicKey.data; final btc.PaymentData? data; switch (input.derivePathType) { @@ -1867,9 +1870,9 @@ mixin SparkInterface txb.sign( vin: i, keyPair: btc.ECPair.fromPrivateKey( - vin[i].keyPair!.privateKey.data, + vin[i].key!.privateKey!.data, network: _bitcoinDartNetwork, - compressed: vin[i].keyPair!.privateKey.compressed, + compressed: vin[i].key!.privateKey!.compressed, ), witnessValue: vin[i].utxo.value, @@ -1916,7 +1919,7 @@ mixin SparkInterface rawValue: nFeeRet, fractionDigits: cryptoCurrency.fractionDigits, ), - usedUTXOs: vin.map((e) => e.utxo).toList(), + usedUTXOs: vin, tempTx: TransactionV2( walletId: walletId, blockHash: null, @@ -2071,7 +2074,8 @@ mixin SparkInterface throw Exception("Attempted send of zero amount"); } - final utxos = txData.utxos; + final utxos = + txData.utxos?.whereType().map((e) => e.utxo).toList(); final bool coinControl = utxos != null; final utxosTotal = @@ -2226,13 +2230,14 @@ mixin SparkInterface txData: TxData( sparkNameInfo: data, recipients: [ - ( + TxRecipient( address: destinationAddress, amount: Amount.fromDecimal( Decimal.fromInt(kStandardSparkNamesFee[name.length] * years), fractionDigits: cryptoCurrency.fractionDigits, ), isChange: false, + addressType: cryptoCurrency.getAddressType(destinationAddress)!, ), ], ), diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 3252be98e..82cc82e0b 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -69,11 +69,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -119,11 +119,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, @@ -169,11 +169,11 @@ class _DesktopFeeDialogState extends ConsumerState { } else if (coin is Firo) { final Amount fee; switch (ref.read(publicPrivateBalanceStateProvider.state).state) { - case FiroType.spark: + case BalanceType.private: fee = await (wallet as FiroWallet).estimateFeeForSpark( amount, ); - case FiroType.public: + case BalanceType.public: fee = await (wallet as FiroWallet).estimateFeeFor( amount, feeRate, diff --git a/lib/widgets/qr_scanner.dart b/lib/widgets/qr_scanner.dart new file mode 100644 index 000000000..66941ac9d --- /dev/null +++ b/lib/widgets/qr_scanner.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import '../themes/stack_colors.dart'; +import '../utilities/logger.dart'; +import '../utilities/text_styles.dart'; +import 'background.dart'; +import 'custom_buttons/app_bar_icon_button.dart'; + +class QrScanner extends ConsumerWidget { + const QrScanner({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: const AppBarBackButton(), + title: Text("Scan QR code", style: STextStyles.navBarTitle(context)), + ), + body: MobileScanner( + onDetect: (capture) { + final data = + ((capture.raw as Map?)?["data"] as List?)?.firstOrNull as Map?; + + final value = + data?["rawValue"] as String? ?? + data?["displayValue"] as String?; + + Navigator.of(context).pop(value); + }, + onDetectError: (error, stackTrace) { + Logging.instance.w( + "Mobile scanner", + error: error, + stackTrace: stackTrace, + ); + Navigator.of(context).pop(); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/textfields/frost_step_field.dart b/lib/widgets/textfields/frost_step_field.dart index 31364555b..f94fac2b4 100644 --- a/lib/widgets/textfields/frost_step_field.dart +++ b/lib/widgets/textfields/frost_step_field.dart @@ -79,7 +79,7 @@ class _FrostStepFieldState extends ConsumerState { await Future.delayed(const Duration(milliseconds: 75)); } - final qrResult = await ref.read(pBarcodeScanner).scan(); + final qrResult = await ref.read(pBarcodeScanner).scan(context: context); widget.controller.text = qrResult.rawContent; diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 8bca8e723..239136a98 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -20,6 +20,7 @@ list(APPEND FLUTTER_FFI_PLUGIN_LIST camera_linux coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2bf8597f..b30b6bead 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -18,6 +18,7 @@ import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs import local_auth_darwin +import mobile_scanner import package_info_plus import path_provider_foundation import share_plus @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index c5dda1160..66cfe0e85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,14 +70,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.12.0" - barcode_scan2: - dependency: "direct main" - description: - name: barcode_scan2 - sha256: efbe38629e6df2200e4d60ebe252e8e041cd5ae7b50f194a20f01779ade9d1c3 - url: "https://pub.dev" - source: hosted - version: "4.5.0" basic_utils: dependency: "direct main" description: @@ -159,10 +151,10 @@ packages: dependency: "direct main" description: name: blockchain_utils - sha256: ebb6c336ba0120de0982c50d8bc597cb494a530bd22bd462895bb5cebde405af + sha256: "1e4f30b98d92f7ccf2eda009a23b53871a1c9b8b6dfa00bb1eb17ec00ae5eeeb" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.6.0" boolean_selector: dependency: transitive description: @@ -361,20 +353,20 @@ packages: dependency: "direct overridden" description: path: coinlib - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 url: "https://www.github.com/julian-CStack/coinlib" source: git - version: "3.1.0" + version: "4.1.0" coinlib_flutter: dependency: "direct main" description: path: coinlib_flutter - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 - resolved-ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 + resolved-ref: da1b3659e296660ac2b36f81d155d2362a2b3195 url: "https://www.github.com/julian-CStack/coinlib" source: git - version: "3.0.0" + version: "4.0.0" collection: dependency: transitive description: @@ -892,7 +884,7 @@ packages: source: git version: "8.3.1" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be @@ -989,6 +981,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.0" + flutter_mwebd: + dependency: "direct main" + description: + name: flutter_mwebd + sha256: f6daecf6a4e10dde0a2fbfe026d801cd41864c6464923d9a26a92c613c893173 + url: "https://pub.dev" + source: hosted + version: "0.0.1-pre.6" flutter_native_splash: dependency: "direct main" description: @@ -1119,8 +1119,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" - resolved-ref: "9dc883f4432c8db4ec44cb8cc836963295d63952" + ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 + resolved-ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 url: "https://github.com/cypherstack/fusiondart.git" source: git version: "1.0.0" @@ -1140,6 +1140,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.5" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" graphs: dependency: transitive description: @@ -1148,6 +1164,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + grpc: + dependency: transitive + description: + name: grpc + sha256: "30e1edae6846b163a64f6d8716e3443980fe1f7d2d1f086f011d24ea186f2582" + url: "https://pub.dev" + source: hosted + version: "4.0.4" hex: dependency: "direct main" description: @@ -1204,6 +1228,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.6" + http2: + dependency: transitive + description: + name: http2 + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" + url: "https://pub.dev" + source: hosted + version: "2.3.1" http_multi_server: dependency: transitive description: @@ -1229,7 +1261,7 @@ packages: source: hosted version: "1.0.3" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d @@ -1482,6 +1514,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "54005bdea7052d792d35b4fef0f84ec5ddc3a844b250ecd48dc192fb9b4ebc95" + url: "https://pub.dev" + source: hosted + version: "7.0.1" mockingjay: dependency: "direct dev" description: @@ -1522,6 +1562,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + mweb_client: + dependency: "direct main" + description: + name: mweb_client + sha256: "263ba560dab7e63a1d03875d455a19cc4a1ab9720786cd9d6ffcc42127d06732" + url: "https://pub.dev" + source: hosted + version: "0.2.0" namecoin: dependency: "direct main" description: @@ -1767,10 +1815,10 @@ packages: dependency: transitive description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: "579fe5557eae58e3adca2e999e38f02441d8aa908703854a9e0a0f47fa857731" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -1843,14 +1891,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" sec: dependency: transitive description: @@ -2513,5 +2553,5 @@ packages: source: hosted version: "0.2.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.7.2 <4.0.0" flutter: ">=3.29.0" diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index e70887c87..4e60e7857 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -70,14 +70,13 @@ dependencies: fusiondart: git: url: https://github.com/cypherstack/fusiondart.git - ref: 9dc883f4432c8db4ec44cb8cc836963295d63952 + ref: afaad488f5215a9c2c211e5e2f8460237eef60f1 # Utility plugins http: ^0.13.0 local_auth: ^2.3.0 permission_handler: ^12.0.0+1 flutter_local_notifications: ^17.2.2 - rxdart: ^0.27.3 zxcvbn: ^1.0.0 dart_numerics: ^0.0.6 @@ -122,7 +121,8 @@ dependencies: event_bus: ^2.0.0 uuid: ^3.0.5 crypto: ^3.0.2 - barcode_scan2: ^4.5.0 + mobile_scanner: ^7.0.1 + image: ^4.3.0 wakelock_plus: ^1.2.8 intl: ^0.17.0 devicelocale: @@ -173,7 +173,11 @@ dependencies: convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 - coinlib_flutter: ^3.0.0 + coinlib_flutter: + git: + url: https://www.github.com/julian-CStack/coinlib + path: coinlib_flutter + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git @@ -220,6 +224,9 @@ dependencies: path: ^1.9.1 cs_salvium: ^1.2.0 cs_salvium_flutter_libs: ^1.0.2 + flutter_mwebd: ^0.0.1-pre.6 + mweb_client: ^0.2.0 + fixnum: ^1.1.1 dev_dependencies: flutter_test: @@ -260,19 +267,17 @@ dependency_overrides: win32: ^5.5.4 # namecoin names lib needs to be updated - #coinlib: ^3.0.0 - #coinlib_flutter: ^3.0.0 coinlib: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 coinlib_flutter: git: url: https://www.github.com/julian-CStack/coinlib path: coinlib_flutter - ref: fd5f658320f00a2e281ccaee97c2d2a77b4aa966 + ref: da1b3659e296660ac2b36f81d155d2362a2b3195 bip47: git: diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 436a918fd..7ea2055fc 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -348,7 +348,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -395,25 +395,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i13.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index 25bfa08e4..192ce703a 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -3,14 +3,14 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i3; import 'package:stackwallet/services/address_book_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -49,29 +49,28 @@ class _FakeContactEntry_1 extends _i1.SmartFake implements _i3.ContactEntry { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [AddressBookService]. @@ -111,15 +110,15 @@ class MockAddressBookService extends _i1.Mock ) as _i3.ContactEntry); @override - _i5.Future> search(String? text) => + _i4.Future> search(String? text) => (super.noSuchMethod( Invocation.method( #search, [text], ), returnValue: - _i5.Future>.value(<_i3.ContactEntry>[]), - ) as _i5.Future>); + _i4.Future>.value(<_i3.ContactEntry>[]), + ) as _i4.Future>); @override bool matches( @@ -138,33 +137,33 @@ class MockAddressBookService extends _i1.Mock ) as bool); @override - _i5.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( + _i4.Future addContact(_i3.ContactEntry? contact) => (super.noSuchMethod( Invocation.method( #addContact, [contact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future editContact(_i3.ContactEntry? editedContact) => + _i4.Future editContact(_i3.ContactEntry? editedContact) => (super.noSuchMethod( Invocation.method( #editContact, [editedContact], ), - returnValue: _i5.Future.value(false), - ) as _i5.Future); + returnValue: _i4.Future.value(false), + ) as _i4.Future); @override - _i5.Future removeContact(String? id) => (super.noSuchMethod( + _i4.Future removeContact(String? id) => (super.noSuchMethod( Invocation.method( #removeContact, [id], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index c68851019..804721d4c 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -209,7 +209,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 08e96b5a3..9218bd83e 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -209,7 +209,7 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -256,25 +256,6 @@ class MockNodeService extends _i1.Mock implements _i6.NodeService { returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override - _i4.Future edit( - _i7.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - @override _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index 3c66bf1f3..68ac1be3e 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -3,15 +3,15 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; +import 'dart:async' as _i4; import 'dart:ui' as _i7; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; import 'package:stackwallet/models/node_model.dart' as _i9; import 'package:stackwallet/services/node_service.dart' as _i8; import 'package:stackwallet/services/wallets_service.dart' as _i6; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i4; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' as _i3; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' @@ -55,29 +55,28 @@ class _FakeSecureStorageInterface_1 extends _i1.SmartFake /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i4.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i5.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i4.Future<_i2.ScanResult> scan({required _i5.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i5.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i5.Future<_i2.ScanResult>); + ) as _i4.Future<_i2.ScanResult>); } /// A class which mocks [WalletsService]. @@ -89,12 +88,12 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { } @override - _i5.Future> get walletNames => + _i4.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i5.Future>.value( + returnValue: _i4.Future>.value( {}), - ) as _i5.Future>); + ) as _i4.Future>); @override bool get hasListeners => (super.noSuchMethod( @@ -175,17 +174,17 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as bool); @override - _i5.Future updateDefaults() => (super.noSuchMethod( + _i4.Future updateDefaults() => (super.noSuchMethod( Invocation.method( #updateDefaults, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setPrimaryNodeFor({ + _i4.Future setPrimaryNodeFor({ required _i10.CryptoCurrency? coin, required _i9.NodeModel? node, bool? shouldNotifyListeners = false, @@ -200,9 +199,9 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override _i9.NodeModel? getPrimaryNodeFor({required _i10.CryptoCurrency? currency}) => @@ -243,26 +242,26 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { ) as List<_i9.NodeModel>); @override - _i5.Future save( + _i4.Future save( _i9.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future delete( + _i4.Future delete( String? id, bool? shouldNotifyListeners, ) => @@ -274,12 +273,12 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future setEnabledState( + _i4.Future setEnabledState( String? id, bool? enabled, bool? shouldNotifyListeners, @@ -293,38 +292,19 @@ class MockNodeService extends _i1.Mock implements _i8.NodeService { shouldNotifyListeners, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i5.Future edit( - _i9.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - - @override - _i5.Future updateCommunityNodes() => (super.noSuchMethod( + _i4.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( #updateCommunityNodes, [], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override void addListener(_i7.VoidCallback? listener) => super.noSuchMethod( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index d6d788327..b4eb99213 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index 6a4ad08ce..787f1f801 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart index b8fee8040..6a2a923e0 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_view_screen_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index 40a295640..d3a16fd4f 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -3,7 +3,7 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'dart:ui' as _i13; import 'package:local_auth/local_auth.dart' as _i7; @@ -11,13 +11,13 @@ import 'package:local_auth_android/local_auth_android.dart' as _i8; import 'package:local_auth_darwin/local_auth_darwin.dart' as _i9; import 'package:local_auth_windows/local_auth_windows.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i6; +import 'package:mockito/src/dummies.dart' as _i4; import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart' as _i3; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i2; import 'package:stackwallet/services/wallets_service.dart' as _i12; import 'package:stackwallet/utilities/biometrics.dart' as _i11; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' - as _i5; + as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -61,33 +61,13 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i2.ElectrumXClient); - @override - _i4.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i5.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( #base64ToHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToHex, @@ -102,7 +82,7 @@ class MockCachedElectrumXClient extends _i1.Mock #base64ToReverseHex, [source], ), - returnValue: _i6.dummyValue( + returnValue: _i4.dummyValue( this, Invocation.method( #base64ToReverseHex, @@ -112,9 +92,9 @@ class MockCachedElectrumXClient extends _i1.Mock ) as String); @override - _i4.Future> getTransaction({ + _i5.Future> getTransaction({ required String? txHash, - required _i5.CryptoCurrency? cryptoCurrency, + required _i6.CryptoCurrency? cryptoCurrency, bool? verbose = true, }) => (super.noSuchMethod( @@ -128,38 +108,21 @@ class MockCachedElectrumXClient extends _i1.Mock }, ), returnValue: - _i4.Future>.value({}), - ) as _i4.Future>); - - @override - _i4.Future> getUsedCoinSerials({ - required _i5.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i4.Future>.value([]), - ) as _i4.Future>); + _i5.Future>.value({}), + ) as _i5.Future>); @override - _i4.Future clearSharedTransactionCache( - {required _i5.CryptoCurrency? cryptoCurrency}) => + _i5.Future clearSharedTransactionCache( + {required _i6.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod( Invocation.method( #clearSharedTransactionCache, [], {#cryptoCurrency: cryptoCurrency}, ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [LocalAuthentication]. @@ -172,13 +135,13 @@ class MockLocalAuthentication extends _i1.Mock } @override - _i4.Future get canCheckBiometrics => (super.noSuchMethod( + _i5.Future get canCheckBiometrics => (super.noSuchMethod( Invocation.getter(#canCheckBiometrics), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? localizedReason, Iterable<_i8.AuthMessages>? authMessages = const [ _i9.IOSAuthMessages(), @@ -197,37 +160,37 @@ class MockLocalAuthentication extends _i1.Mock #options: options, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future stopAuthentication() => (super.noSuchMethod( + _i5.Future stopAuthentication() => (super.noSuchMethod( Invocation.method( #stopAuthentication, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future isDeviceSupported() => (super.noSuchMethod( + _i5.Future isDeviceSupported() => (super.noSuchMethod( Invocation.method( #isDeviceSupported, [], ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); @override - _i4.Future> getAvailableBiometrics() => + _i5.Future> getAvailableBiometrics() => (super.noSuchMethod( Invocation.method( #getAvailableBiometrics, [], ), returnValue: - _i4.Future>.value(<_i8.BiometricType>[]), - ) as _i4.Future>); + _i5.Future>.value(<_i8.BiometricType>[]), + ) as _i5.Future>); } /// A class which mocks [Biometrics]. @@ -239,7 +202,7 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { } @override - _i4.Future authenticate({ + _i5.Future authenticate({ required String? cancelButtonText, required String? localizedReason, required String? title, @@ -254,8 +217,8 @@ class MockBiometrics extends _i1.Mock implements _i11.Biometrics { #title: title, }, ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i5.Future.value(false), + ) as _i5.Future); } /// A class which mocks [WalletsService]. @@ -267,12 +230,12 @@ class MockWalletsService extends _i1.Mock implements _i12.WalletsService { } @override - _i4.Future> get walletNames => + _i5.Future> get walletNames => (super.noSuchMethod( Invocation.getter(#walletNames), - returnValue: _i4.Future>.value( + returnValue: _i5.Future>.value( {}), - ) as _i4.Future>); + ) as _i5.Future>); @override bool get hasListeners => (super.noSuchMethod( diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index a7a3ad695..cd4db9558 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -3,11 +3,11 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; -import 'package:barcode_scan2/barcode_scan2.dart' as _i2; +import 'package:flutter/material.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i3; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -36,27 +36,26 @@ class _FakeScanResult_0 extends _i1.SmartFake implements _i2.ScanResult { /// /// See the documentation for Mockito's code generation for more information. class MockBarcodeScannerWrapper extends _i1.Mock - implements _i3.BarcodeScannerWrapper { + implements _i2.BarcodeScannerWrapper { MockBarcodeScannerWrapper() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i2.ScanResult> scan( - {_i2.ScanOptions? options = const _i2.ScanOptions()}) => + _i3.Future<_i2.ScanResult> scan({required _i4.BuildContext? context}) => (super.noSuchMethod( Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), - returnValue: _i4.Future<_i2.ScanResult>.value(_FakeScanResult_0( + returnValue: _i3.Future<_i2.ScanResult>.value(_FakeScanResult_0( this, Invocation.method( #scan, [], - {#options: options}, + {#context: context}, ), )), - ) as _i4.Future<_i2.ScanResult>); + ) as _i3.Future<_i2.ScanResult>); } diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index c840f4b50..35fbc67a1 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 50706b9cc..7c455f73d 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 93185c901..86090d418 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index 287146e12..a397b611a 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 8a1cce451..861fa4327 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -733,26 +733,6 @@ class MockCachedElectrumXClient extends _i1.Mock ), ) as _i5.ElectrumXClient); - @override - _i8.Future> getAnonymitySet({ - required String? groupId, - String? blockhash = r'', - required _i2.CryptoCurrency? cryptoCurrency, - }) => - (super.noSuchMethod( - Invocation.method( - #getAnonymitySet, - [], - { - #groupId: groupId, - #blockhash: blockhash, - #cryptoCurrency: cryptoCurrency, - }, - ), - returnValue: - _i8.Future>.value({}), - ) as _i8.Future>); - @override String base64ToHex(String? source) => (super.noSuchMethod( Invocation.method( @@ -803,23 +783,6 @@ class MockCachedElectrumXClient extends _i1.Mock _i8.Future>.value({}), ) as _i8.Future>); - @override - _i8.Future> getUsedCoinSerials({ - required _i2.CryptoCurrency? cryptoCurrency, - int? startNumber = 0, - }) => - (super.noSuchMethod( - Invocation.method( - #getUsedCoinSerials, - [], - { - #cryptoCurrency: cryptoCurrency, - #startNumber: startNumber, - }, - ), - returnValue: _i8.Future>.value([]), - ) as _i8.Future>); - @override _i8.Future clearSharedTransactionCache( {required _i2.CryptoCurrency? cryptoCurrency}) => diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index 4b1944af8..410094cdf 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -1263,7 +1263,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1310,25 +1310,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i23.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_card_test.mocks.dart b/test/widget_tests/node_card_test.mocks.dart index 808f10fab..19c8c5166 100644 --- a/test/widget_tests/node_card_test.mocks.dart +++ b/test/widget_tests/node_card_test.mocks.dart @@ -149,7 +149,7 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -196,25 +196,6 @@ class MockNodeService extends _i1.Mock implements _i3.NodeService { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); - @override - _i5.Future edit( - _i4.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index d01578f3f..9b7d99828 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -1067,7 +1067,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -1114,25 +1114,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - @override - _i10.Future edit( - _i19.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - @override _i10.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index a090e757e..cd8edb2ab 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1976,17 +1976,6 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { returnValue: _i10.Future.value(), returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); - - @override - _i10.Future getHighestUsedMintIndex({required String? walletId}) => - (super.noSuchMethod( - Invocation.method( - #getHighestUsedMintIndex, - [], - {#walletId: walletId}, - ), - returnValue: _i10.Future.value(), - ) as _i10.Future); } /// A class which mocks [IThemeAssets]. diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index fc2c10fbb..d406a0c35 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -316,7 +316,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -363,25 +363,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); - @override - _i8.Future edit( - _i11.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i8.Future.value(), - returnValueForMissingStub: _i8.Future.value(), - ) as _i8.Future); - @override _i8.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 5f16a8a2e..6018c1f93 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -456,7 +456,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) => (super.noSuchMethod( Invocation.method( - #add, + #save, [ node, password, @@ -503,25 +503,6 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); - @override - _i9.Future edit( - _i15.NodeModel? editedNode, - String? password, - bool? shouldNotifyListeners, - ) => - (super.noSuchMethod( - Invocation.method( - #edit, - [ - editedNode, - password, - shouldNotifyListeners, - ], - ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); - @override _i9.Future updateCommunityNodes() => (super.noSuchMethod( Invocation.method( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b7bea9e3f..ac0ec291d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -23,6 +23,7 @@ list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST coinlib_flutter flutter_libsparkmobile + flutter_mwebd frostdart tor_ffi_plugin xelis_flutter