diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index a16fb2b9a..6f52a58e3 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -19,6 +19,8 @@ import '../../models/isar/models/contact_entry.dart'; import '../../models/isar/models/isar_models.dart'; import '../../models/isar/ordinal.dart'; import '../../models/isar/stack_theme.dart'; +import '../../models/isar/models/silent_payments/silent_payment_config.dart'; +import '../../models/isar/models/silent_payments/silent_payment_metadata.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/extensions/extensions.dart'; import '../../utilities/stack_file_system.dart'; @@ -70,6 +72,8 @@ class MainDB { WalletInfoMetaSchema, TokenWalletInfoSchema, FrostWalletInfoSchema, + SilentPaymentConfigSchema, + SilentPaymentMetadataSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, diff --git a/lib/models/isar/models/silent_payments/silent_payment_config.dart b/lib/models/isar/models/silent_payments/silent_payment_config.dart new file mode 100644 index 000000000..7557619f7 --- /dev/null +++ b/lib/models/isar/models/silent_payments/silent_payment_config.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; +import 'package:isar/isar.dart'; +import '../../../../wallets/isar/isar_id_interface.dart'; + +part 'silent_payment_config.g.dart'; + +@Collection(accessor: "silentPaymentConfig", inheritance: false) +class SilentPaymentConfig implements IsarId { + @override + Id id = Isar.autoIncrement; + + @Index(unique: true) + final String walletId; + + final bool isEnabled; + + final int lastScannedHeight; + + // Store any computed labels for efficient lookup + final String? labelMapJsonString; + + @ignore + Map? get labelMap => + labelMapJsonString == null + ? null + : Map.from(jsonDecode(labelMapJsonString!) as Map); + + SilentPaymentConfig({ + required this.walletId, + this.isEnabled = false, + this.lastScannedHeight = 0, + this.labelMapJsonString, + }); + + SilentPaymentConfig copyWith({ + bool? isEnabled, + int? lastScannedHeight, + String? labelMapJsonString, + }) { + return SilentPaymentConfig( + walletId: walletId, + isEnabled: isEnabled ?? this.isEnabled, + lastScannedHeight: lastScannedHeight ?? this.lastScannedHeight, + labelMapJsonString: labelMapJsonString ?? this.labelMapJsonString, + )..id = id; + } + + Future updateEnabled({ + required bool enabled, + required Isar isar, + }) async { + if (isEnabled != enabled) { + await isar.writeTxn(() async { + await isar.silentPaymentConfig.put(copyWith(isEnabled: enabled)); + }); + } + } + + Future updateLastScannedHeight({ + required int height, + required Isar isar, + }) async { + if (lastScannedHeight != height) { + await isar.writeTxn(() async { + await isar.silentPaymentConfig.put(copyWith(lastScannedHeight: height)); + }); + } + } + + Future updateLabelMap({ + required Map newLabelMap, + required Isar isar, + }) async { + final encodedMap = jsonEncode(newLabelMap); + + if (labelMapJsonString != encodedMap) { + await isar.writeTxn(() async { + await isar.silentPaymentConfig.put( + copyWith(labelMapJsonString: encodedMap), + ); + }); + } + } +} diff --git a/lib/models/isar/models/silent_payments/silent_payment_config.g.dart b/lib/models/isar/models/silent_payments/silent_payment_config.g.dart new file mode 100644 index 000000000..c1673021e --- /dev/null +++ b/lib/models/isar/models/silent_payments/silent_payment_config.g.dart @@ -0,0 +1,949 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'silent_payment_config.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetSilentPaymentConfigCollection on Isar { + IsarCollection get silentPaymentConfig => + this.collection(); +} + +const SilentPaymentConfigSchema = CollectionSchema( + name: r'SilentPaymentConfig', + id: 8185245117812277101, + properties: { + r'isEnabled': PropertySchema( + id: 0, + name: r'isEnabled', + type: IsarType.bool, + ), + r'labelMapJsonString': PropertySchema( + id: 1, + name: r'labelMapJsonString', + type: IsarType.string, + ), + r'lastScannedHeight': PropertySchema( + id: 2, + name: r'lastScannedHeight', + type: IsarType.long, + ), + r'walletId': PropertySchema( + id: 3, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _silentPaymentConfigEstimateSize, + serialize: _silentPaymentConfigSerialize, + deserialize: _silentPaymentConfigDeserialize, + deserializeProp: _silentPaymentConfigDeserializeProp, + idName: r'id', + indexes: { + r'walletId': IndexSchema( + id: -1783113319798776304, + name: r'walletId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _silentPaymentConfigGetId, + getLinks: _silentPaymentConfigGetLinks, + attach: _silentPaymentConfigAttach, + version: '3.1.8', +); + +int _silentPaymentConfigEstimateSize( + SilentPaymentConfig object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.labelMapJsonString; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _silentPaymentConfigSerialize( + SilentPaymentConfig object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeBool(offsets[0], object.isEnabled); + writer.writeString(offsets[1], object.labelMapJsonString); + writer.writeLong(offsets[2], object.lastScannedHeight); + writer.writeString(offsets[3], object.walletId); +} + +SilentPaymentConfig _silentPaymentConfigDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SilentPaymentConfig( + isEnabled: reader.readBoolOrNull(offsets[0]) ?? false, + labelMapJsonString: reader.readStringOrNull(offsets[1]), + lastScannedHeight: reader.readLongOrNull(offsets[2]) ?? 0, + walletId: reader.readString(offsets[3]), + ); + object.id = id; + return object; +} + +P _silentPaymentConfigDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readBoolOrNull(offset) ?? false) as P; + case 1: + return (reader.readStringOrNull(offset)) as P; + case 2: + return (reader.readLongOrNull(offset) ?? 0) as P; + case 3: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _silentPaymentConfigGetId(SilentPaymentConfig object) { + return object.id; +} + +List> _silentPaymentConfigGetLinks( + SilentPaymentConfig object) { + return []; +} + +void _silentPaymentConfigAttach( + IsarCollection col, Id id, SilentPaymentConfig object) { + object.id = id; +} + +extension SilentPaymentConfigByIndex on IsarCollection { + Future getByWalletId(String walletId) { + return getByIndex(r'walletId', [walletId]); + } + + SilentPaymentConfig? getByWalletIdSync(String walletId) { + return getByIndexSync(r'walletId', [walletId]); + } + + Future deleteByWalletId(String walletId) { + return deleteByIndex(r'walletId', [walletId]); + } + + bool deleteByWalletIdSync(String walletId) { + return deleteByIndexSync(r'walletId', [walletId]); + } + + Future> getAllByWalletId( + List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndex(r'walletId', values); + } + + List getAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'walletId', values); + } + + Future deleteAllByWalletId(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'walletId', values); + } + + int deleteAllByWalletIdSync(List walletIdValues) { + final values = walletIdValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'walletId', values); + } + + Future putByWalletId(SilentPaymentConfig object) { + return putByIndex(r'walletId', object); + } + + Id putByWalletIdSync(SilentPaymentConfig object, {bool saveLinks = true}) { + return putByIndexSync(r'walletId', object, saveLinks: saveLinks); + } + + Future> putAllByWalletId(List objects) { + return putAllByIndex(r'walletId', objects); + } + + List putAllByWalletIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'walletId', objects, saveLinks: saveLinks); + } +} + +extension SilentPaymentConfigQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SilentPaymentConfigQueryWhere + on QueryBuilder { + QueryBuilder + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId', + value: [walletId], + )); + }); + } + + QueryBuilder + walletIdNotEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )); + } + }); + } +} + +extension SilentPaymentConfigQueryFilter on QueryBuilder { + QueryBuilder + idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder + idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + isEnabledEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isEnabled', + value: value, + )); + }); + } + + QueryBuilder + labelMapJsonStringIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'labelMapJsonString', + )); + }); + } + + QueryBuilder + labelMapJsonStringIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'labelMapJsonString', + )); + }); + } + + QueryBuilder + labelMapJsonStringEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'labelMapJsonString', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'labelMapJsonString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'labelMapJsonString', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMapJsonStringIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'labelMapJsonString', + value: '', + )); + }); + } + + QueryBuilder + labelMapJsonStringIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'labelMapJsonString', + value: '', + )); + }); + } + + QueryBuilder + lastScannedHeightEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastScannedHeight', + value: value, + )); + }); + } + + QueryBuilder + lastScannedHeightGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastScannedHeight', + value: value, + )); + }); + } + + QueryBuilder + lastScannedHeightLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastScannedHeight', + value: value, + )); + }); + } + + QueryBuilder + lastScannedHeightBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastScannedHeight', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension SilentPaymentConfigQueryObject on QueryBuilder {} + +extension SilentPaymentConfigQueryLinks on QueryBuilder {} + +extension SilentPaymentConfigQuerySortBy + on QueryBuilder { + QueryBuilder + sortByIsEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isEnabled', Sort.asc); + }); + } + + QueryBuilder + sortByIsEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isEnabled', Sort.desc); + }); + } + + QueryBuilder + sortByLabelMapJsonString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'labelMapJsonString', Sort.asc); + }); + } + + QueryBuilder + sortByLabelMapJsonStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'labelMapJsonString', Sort.desc); + }); + } + + QueryBuilder + sortByLastScannedHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastScannedHeight', Sort.asc); + }); + } + + QueryBuilder + sortByLastScannedHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastScannedHeight', Sort.desc); + }); + } + + QueryBuilder + sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SilentPaymentConfigQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByIsEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isEnabled', Sort.asc); + }); + } + + QueryBuilder + thenByIsEnabledDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isEnabled', Sort.desc); + }); + } + + QueryBuilder + thenByLabelMapJsonString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'labelMapJsonString', Sort.asc); + }); + } + + QueryBuilder + thenByLabelMapJsonStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'labelMapJsonString', Sort.desc); + }); + } + + QueryBuilder + thenByLastScannedHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastScannedHeight', Sort.asc); + }); + } + + QueryBuilder + thenByLastScannedHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastScannedHeight', Sort.desc); + }); + } + + QueryBuilder + thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder + thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SilentPaymentConfigQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByIsEnabled() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isEnabled'); + }); + } + + QueryBuilder + distinctByLabelMapJsonString({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'labelMapJsonString', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByLastScannedHeight() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastScannedHeight'); + }); + } + + QueryBuilder + distinctByWalletId({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension SilentPaymentConfigQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + isEnabledProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isEnabled'); + }); + } + + QueryBuilder + labelMapJsonStringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'labelMapJsonString'); + }); + } + + QueryBuilder + lastScannedHeightProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastScannedHeight'); + }); + } + + QueryBuilder + walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/models/isar/models/silent_payments/silent_payment_metadata.dart b/lib/models/isar/models/silent_payments/silent_payment_metadata.dart new file mode 100644 index 000000000..19c140624 --- /dev/null +++ b/lib/models/isar/models/silent_payments/silent_payment_metadata.dart @@ -0,0 +1,29 @@ +import 'package:isar/isar.dart'; +import '../../../../wallets/isar/isar_id_interface.dart'; + +part 'silent_payment_metadata.g.dart'; + +@Collection(accessor: "silentPaymentMetadata", inheritance: false) +class SilentPaymentMetadata implements IsarId { + /// Primary key for this metadata entry. + /// Should be generated as a sha256 hash of '$txid:$vout:$walletId' + @override + Id id; + + /// Private key tweak needed to spend this output + final String tweak; + + /// This is not a user-defined label but a cryptographic tag + /// that affects key derivation + final String? label; + + SilentPaymentMetadata({required this.id, required this.tweak, this.label}); + + SilentPaymentMetadata copyWith({int? id, String? tweak, String? label}) { + return SilentPaymentMetadata( + id: id ?? this.id, + tweak: tweak ?? this.tweak, + label: label ?? this.label, + ); + } +} diff --git a/lib/models/isar/models/silent_payments/silent_payment_metadata.g.dart b/lib/models/isar/models/silent_payments/silent_payment_metadata.g.dart new file mode 100644 index 000000000..e4e9dbf77 --- /dev/null +++ b/lib/models/isar/models/silent_payments/silent_payment_metadata.g.dart @@ -0,0 +1,670 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'silent_payment_metadata.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types + +extension GetSilentPaymentMetadataCollection on Isar { + IsarCollection get silentPaymentMetadata => + this.collection(); +} + +const SilentPaymentMetadataSchema = CollectionSchema( + name: r'SilentPaymentMetadata', + id: -6055173424802135633, + properties: { + r'label': PropertySchema( + id: 0, + name: r'label', + type: IsarType.string, + ), + r'tweak': PropertySchema( + id: 1, + name: r'tweak', + type: IsarType.string, + ) + }, + estimateSize: _silentPaymentMetadataEstimateSize, + serialize: _silentPaymentMetadataSerialize, + deserialize: _silentPaymentMetadataDeserialize, + deserializeProp: _silentPaymentMetadataDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _silentPaymentMetadataGetId, + getLinks: _silentPaymentMetadataGetLinks, + attach: _silentPaymentMetadataAttach, + version: '3.1.8', +); + +int _silentPaymentMetadataEstimateSize( + SilentPaymentMetadata object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.label; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.tweak.length * 3; + return bytesCount; +} + +void _silentPaymentMetadataSerialize( + SilentPaymentMetadata object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.label); + writer.writeString(offsets[1], object.tweak); +} + +SilentPaymentMetadata _silentPaymentMetadataDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SilentPaymentMetadata( + id: id, + label: reader.readStringOrNull(offsets[0]), + tweak: reader.readString(offsets[1]), + ); + return object; +} + +P _silentPaymentMetadataDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _silentPaymentMetadataGetId(SilentPaymentMetadata object) { + return object.id; +} + +List> _silentPaymentMetadataGetLinks( + SilentPaymentMetadata object) { + return []; +} + +void _silentPaymentMetadataAttach( + IsarCollection col, Id id, SilentPaymentMetadata object) { + object.id = id; +} + +extension SilentPaymentMetadataQueryWhereSort + on QueryBuilder { + QueryBuilder + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SilentPaymentMetadataQueryWhere on QueryBuilder { + QueryBuilder + idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder + idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder + idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder + idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder + idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } +} + +extension SilentPaymentMetadataQueryFilter on QueryBuilder< + SilentPaymentMetadata, SilentPaymentMetadata, QFilterCondition> { + QueryBuilder idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder labelIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'label', + )); + }); + } + + QueryBuilder labelIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'label', + )); + }); + } + + QueryBuilder labelEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'label', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + labelMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'label', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder labelIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'label', + value: '', + )); + }); + } + + QueryBuilder labelIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'label', + value: '', + )); + }); + } + + QueryBuilder tweakEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'tweak', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tweakContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'tweak', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tweakMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'tweak', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tweakIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tweak', + value: '', + )); + }); + } + + QueryBuilder tweakIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'tweak', + value: '', + )); + }); + } +} + +extension SilentPaymentMetadataQueryObject on QueryBuilder< + SilentPaymentMetadata, SilentPaymentMetadata, QFilterCondition> {} + +extension SilentPaymentMetadataQueryLinks on QueryBuilder {} + +extension SilentPaymentMetadataQuerySortBy + on QueryBuilder { + QueryBuilder + sortByLabel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.asc); + }); + } + + QueryBuilder + sortByLabelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.desc); + }); + } + + QueryBuilder + sortByTweak() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tweak', Sort.asc); + }); + } + + QueryBuilder + sortByTweakDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tweak', Sort.desc); + }); + } +} + +extension SilentPaymentMetadataQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder + thenByLabel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.asc); + }); + } + + QueryBuilder + thenByLabelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.desc); + }); + } + + QueryBuilder + thenByTweak() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tweak', Sort.asc); + }); + } + + QueryBuilder + thenByTweakDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tweak', Sort.desc); + }); + } +} + +extension SilentPaymentMetadataQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByLabel({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'label', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByTweak({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'tweak', caseSensitive: caseSensitive); + }); + } +} + +extension SilentPaymentMetadataQueryProperty on QueryBuilder< + SilentPaymentMetadata, SilentPaymentMetadata, QQueryProperty> { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder + labelProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'label'); + }); + } + + QueryBuilder + tweakProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'tweak'); + }); + } +} diff --git a/lib/pages/silent_payments/silent_payments_view.dart b/lib/pages/silent_payments/silent_payments_view.dart new file mode 100644 index 000000000..0b39932d7 --- /dev/null +++ b/lib/pages/silent_payments/silent_payments_view.dart @@ -0,0 +1,1004 @@ +// ignore_for_file: unused_import, prefer_const_constructors, avoid_print + +import 'dart:async'; + +import 'package:coinlib_flutter/coinlib_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; + +import '../../../wallets/wallet/impl/bitcoin_wallet.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../models/isar/models/isar_models.dart'; +import '../../providers/silent_payment/silent_payment_provider.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/crypto_currency/crypto_currency.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../services/silentium_api.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/extensions/extensions.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_loading_overlay.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/icon_widgets/copy_icon.dart'; +import '../../widgets/rounded_white_container.dart'; + +import 'package:silent_payments/silent_payments.dart'; +import 'package:coinlib/src/tx/outpoint.dart'; + +class SilentPaymentsView extends ConsumerStatefulWidget { + const SilentPaymentsView({super.key, required this.walletId}); + + final String walletId; + + static const String routeName = "/silentPayments"; + + @override + ConsumerState createState() => _SilentPaymentsViewState(); +} + +class _SilentPaymentsViewState extends ConsumerState { + bool _enabled = false; + + // Mock silent payment owner + SilentPaymentOwner? _owner; + String _silentPaymentAddress = ""; + String _scanPrivateKey = ""; + String _spendPrivateKey = ""; + + // Mock send fields + final TextEditingController _recipientAddressController = + TextEditingController(); + final TextEditingController _amountController = TextEditingController(); + + // Mock scanning fields + final TextEditingController _blockHeightController = TextEditingController(); + final List _detectedOutputs = []; + + // Mock debug information + final Map _debugInfo = {}; + + @override + void initState() { + super.initState(); + _blockHeightController.text = "800000"; // Example block height + _amountController.text = "0.0001"; // Example amount + _loadWalletData(); + } + + Future _loadWalletData() async { + final wallet = ref.read(pWallets).getWallet(widget.walletId); + if (wallet is BitcoinWallet) { + final rootNode = await wallet.getRootHDNode(); + final owner = SilentPaymentOwner.fromBip32(rootNode); + + // Translate the wallet's network to what the Silent Payment library expects + final network = switch (wallet.info.coin.network) { + CryptoCurrencyNetwork.main => 'BitcoinNetwork.mainnet', + CryptoCurrencyNetwork.test => 'BitcoinNetwork.testnet', + CryptoCurrencyNetwork.test4 => 'BitcoinNetwork.testnet', + _ => null, + }; + + _owner = owner; + _silentPaymentAddress = owner.toString(network: network); + _scanPrivateKey = owner.b_scan.data.toHex; + _spendPrivateKey = owner.b_spend.data.toHex; + + // Initialize UI state from config + final config = ref.read(pSilentPaymentConfig(widget.walletId)); + + setState(() { + _enabled = config.isEnabled; + }); + + // Update debug info + _debugInfo.clear(); + _debugInfo.addAll({ + "b_scan": _scanPrivateKey, + "b_spend": _spendPrivateKey, + }); + } + } + + @override + void dispose() { + _recipientAddressController.dispose(); + _amountController.dispose(); + _blockHeightController.dispose(); + super.dispose(); + } + + // Example: Toggle silent payments on/off + Future _toggleSilentPayments(bool enabled) async { + final isar = ref.read(mainDBProvider).isar; + final config = ref.read(pSilentPaymentConfig(widget.walletId)); + await config.updateEnabled(enabled: enabled, isar: isar); + + // Update the local state to reflect the change + setState(() { + _enabled = enabled; + }); + } + + // Mock sending silent payment + Future _sendSilentPayment() async { + final recipientAddress = _recipientAddressController.text.trim(); + final amount = _amountController.text.trim(); + + if (recipientAddress.isEmpty) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Please enter a recipient address", + context: context, + ); + return; + } + + if (amount.isEmpty || + double.tryParse(amount) == null || + double.parse(amount) <= 0) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Please enter a valid amount", + context: context, + ); + return; + } + + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (_) { + return WillPopScope( + onWillPop: () async => shouldPop, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), + child: const CustomLoadingOverlay( + message: "Sending Silent Payment", + eventBus: null, + ), + ), + ); + }, + ), + ); + + try { + // In a real implementation, this would: + // 1. Create a transaction with inputs + // 2. Derive the shared secret + // 3. Generate output addresses + // 4. Broadcast the transaction + + // Get Bitcoin wallet instance + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as BitcoinWallet; + final mainDB = wallet.mainDB; + final root = await wallet.getRootHDNode(); + + // Amount in satoshis + final satoshiAmount = (double.parse(amount) * 100000000).toInt(); + + final silentium = SilentiumApi(baseUrl: "https://bitcoin.silentium.dev/"); + final chainTip = await silentium.getLatestBlockHeight(); + final scalars = await silentium.getScalarsForBlock(883586); + + // might not need any of this here + + // Get available UTXOs + // final isar = mainDB.isar; + // final utxos = + // await isar.utxos + // .filter() + // .walletIdEqualTo(widget.walletId) + // .usedEqualTo(false) + // .findAll(); + // final utxos = + // await mainDB + // .getUTXOs(widget.walletId) + // .filter() + // .usedEqualTo(false) + // .findAll(); + // + // final signingData = await wallet.fetchBuildTxData(utxos); + // + // final selectedUtxos = []; + // int runningTotal = 0; + // final int estimatedFee = 1000; + // + // for (final utxo in utxos) { + // selectedUtxos.add(utxo); + // runningTotal += utxo.value; + // + // if (runningTotal >= satoshiAmount + estimatedFee) { + // break; + // } + // } + // + // // Convert UTXOs to outpoints + // final outpoints = + // selectedUtxos.map((utxo) { + // return OutPoint(utxo.txid.toUint8ListFromHex, utxo.vout); + // }).toList(); + // + // final inputPrivateInfos = + // selectedUtxos.map((utxo) async { + // // TODO: figure out how to get private key for each outpoint + // final address = await mainDB.getAddress( + // wallet.walletId, + // utxo.address!, + // ); + // final keys = root.derivePath(address.derivationPath!.value); + // final ECPrivateKey privkey; + // final bool isTaproot; + // return ECPrivateInfo(privkey, isTaproot); + // }).toList(); + // + // // Prepare destination addresses + // final destinations = [ + // SilentPaymentDestination.fromAddress(recipientAddress, 0), + // ]; + // + // final inputPubKeys = + // inputPrivateInfos.map((info) => info.privkey.pubkey).toList(); + // + // final builder = SilentPaymentBuilder( + // outpoints: outpoints, + // publicKeys: inputPubKeys, + // ); + // + // final outputMap = builder.createOutputs(inputPrivateInfos, destinations); + // final sendingOutputs = + // outputMap.values + // .expand((outputs) => outputs.map((o) => o.address.data.toHex)) + // .toList(); + + await Future.delayed(const Duration(seconds: 1)); + + // Mock transaction details for demonstration + final txDetails = { + "txid": + "85a12304f5d6e14ad32b6ebe2cf39f238695c9479c55d6291c2a1fe2c4af56a3", + "outpoints": [ + "c45fc3c36d30c9b93b288e51ef59f4af6e963d8e100c752b212aeB65db2cc604:0", + "7ef3c7f4a1e247b47b1fcb075e4639955ea4c9c6674b42b8b858cbf1f738d352:1", + ], + "outputs": [ + "bc1p5gv9zay9m8w99dqeyl2p5zazg4fcw4nnfxazqpu8xre8ctmrx7aq5mhzz7", + "bc1pcqtmhrfe95e5nejx98u6kcfuxz8lnm976lmedrjxvllr4qm5t5tsl7xnw3", + ], + "amount": double.parse(amount), + "fee": 0.00001, + }; + + // Update debug info + _debugInfo.clear(); + _debugInfo.addAll({ + "Destination": recipientAddress, + "Amount": "$amount BTC", + "Transaction ID": txDetails["txid"]! as String, + "Output Addresses": (txDetails["outputs"] as List).join("\n"), + "Chain Tip": chainTip.toString(), + "Scalars": scalars.join("\n"), + }); + + if (mounted) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Silent Payment sent successfully!", + context: context, + ); + } + } catch (e) { + if (mounted) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to send Silent Payment: ${e.toString()}", + context: context, + ); + } + } + + shouldPop = true; + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } + + // Mock scanning for received silent payments + Future _scanForSilentPayments() async { + if (_owner == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Please generate Silent Payment keys first", + context: context, + ); + return; + } + + final blockHeight = _blockHeightController.text.trim(); + + if (blockHeight.isEmpty || int.tryParse(blockHeight) == null) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Please enter a valid block height", + context: context, + ); + return; + } + + setState(() { + _detectedOutputs.clear(); + }); + + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (_) { + return WillPopScope( + onWillPop: () async => shouldPop, + child: Container( + color: Theme.of( + context, + ).extension()!.overlay.withOpacity(0.5), + child: const CustomLoadingOverlay( + message: "Scanning...", + eventBus: null, + ), + ), + ); + }, + ), + ); + + try { + // In a real implementation, this would: + // 1. Fetch block data for the given height + // 2. Extract transactions + // 3. For each transaction, calculate outpoints and input public keys + // 4. Derive shared secrets and scan outputs + + await Future.delayed(const Duration(seconds: 2)); + + // Mock detected outputs for demonstration + if (_enabled) { + _detectedOutputs.clear(); + _detectedOutputs.addAll([ + "bc1p5cyxm5c6rvfqzkjq40rrs7cnjr9qpkpz0qjmv3qr7v9dhm0rcsqsxp8hx8", + "bc1pws9t458yt9fq3ae43a9d23l0tp70yd64qme3pdhtg4rkgkut075st7qcux", + ]); + + // Update debug info + _debugInfo.clear(); + _debugInfo.addAll({ + "Block Height": blockHeight, + "Txs Scanned": "285", + "Inputs Analyzed": "843", + "Outputs Checked": "976", + "Shared Secrets": "123", + }); + } else { + _debugInfo.clear(); + _debugInfo.addAll({ + "Status": + "Scanning disabled - Enable scanning to detect Silent Payments", + }); + } + + if (mounted) { + if (_enabled) { + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Found ${_detectedOutputs.length} Silent Payment outputs", + context: context, + ); + } else { + await showFloatingFlushBar( + type: FlushBarType.info, + message: + "Scanning disabled - Enable scanning to detect Silent Payments", + context: context, + ); + } + } + } catch (e) { + if (mounted) { + await showFloatingFlushBar( + type: FlushBarType.warning, + message: "Failed to scan for Silent Payments: ${e.toString()}", + context: context, + ); + } + } + + shouldPop = true; + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + final colors = Theme.of(context).extension()!; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: + isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: colors.popupBG, + leading: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 24, right: 20), + child: AppBarIconButton( + size: 32, + color: colors.textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + colorFilter: ColorFilter.mode( + colors.topNavIconPrimary, + BlendMode.srcIn, + ), + ), + onPressed: Navigator.of(context).pop, + ), + ), + Text( + "Silent Payments", + style: STextStyles.desktopH3(context), + ), + ], + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Silent Payments", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea(child: _buildContent(isDesktop, colors)), + ); + } + + Widget _buildContent(bool isDesktop, StackColors colors) { + // Use consistent padding approach for both platforms with different values + final double horizontalPadding = isDesktop ? 24 : 16; + final double verticalGap = isDesktop ? 24 : 16; + + return SingleChildScrollView( + padding: EdgeInsets.all(horizontalPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildAddressSection(isDesktop, colors), + + SizedBox(height: verticalGap), + + // Main content - desktop uses rows, mobile uses columns + if (isDesktop) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildSendSection(isDesktop, colors)), + SizedBox(width: horizontalPadding), + Expanded(child: _buildScanSection(isDesktop, colors)), + ], + ) + else + Column( + children: [ + _buildSendSection(isDesktop, colors), + SizedBox(height: verticalGap), + _buildScanSection(isDesktop, colors), + ], + ), + + // Debug information + if (_debugInfo.isNotEmpty) ...[ + SizedBox(height: verticalGap), + _buildDebugSection(isDesktop, colors), + ], + ], + ), + ); + } + + Widget _buildAddressSection(bool isDesktop, StackColors colors) { + // For mobile, if no address is available, return empty widget + if (!isDesktop && _silentPaymentAddress.isEmpty) { + return const SizedBox.shrink(); + } + + // Use RoundedWhiteContainer for desktop, simple column for mobile + Widget content = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Switch( + value: _enabled, + onChanged: (value) async { + await _toggleSilentPayments(value); + }, + activeColor: colors.accentColorGreen, + ), + const SizedBox(width: 12), + Text( + "Scan for Silent Payments", + style: + isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.titleBold12(context), + ), + ], + ), + + if (_silentPaymentAddress.isNotEmpty) ...[ + SizedBox(height: isDesktop ? 16 : 12), + Text( + "Your Silent Payment Address:", + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.smallMed12(context), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: colors.textFieldDefaultBG, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _silentPaymentAddress, + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + ), + const SizedBox(width: 8), + SecondaryButton( + label: "Copy", + buttonHeight: ButtonHeight.m, + iconSpacing: 8, + icon: CopyIcon( + width: 12, + height: 12, + color: + Theme.of( + context, + ).extension()!.buttonTextSecondary, + ), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: _silentPaymentAddress), + ); + if (context.mounted) { + await showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + } + }, + ), + ], + ), + ], + ], + ); + + // On desktop, wrap in RoundedWhiteContainer, on mobile use the column directly + return isDesktop + ? RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: content, + ) + : content; + } + + Widget _buildSendSection(bool isDesktop, StackColors colors) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Send Silent Payment", + style: + isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.titleBold12(context), + ), + const SizedBox(height: 16), + + // Recipient address + Text( + "Recipient Address:", + style: + isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.smallMed12(context), + ), + const SizedBox(height: 8), + TextField( + controller: _recipientAddressController, + decoration: InputDecoration( + hintText: "tsp1...", + filled: true, + fillColor: colors.textFieldDefaultBG, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.smallMed12(context), + minLines: 3, + maxLines: 3, + ), + + const SizedBox(height: 16), + + // Amount + Text( + "Amount (BTC):", + style: + isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.smallMed12(context), + ), + const SizedBox(height: 8), + TextField( + controller: _amountController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + hintText: "0.0001", + filled: true, + fillColor: colors.textFieldDefaultBG, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.smallMed12(context), + ), + + const SizedBox(height: 24), + + // Send button + Center( + child: PrimaryButton( + width: 150, + label: "Send", + onPressed: _sendSilentPayment, + ), + ), + + const SizedBox(height: 16), + + // Send process explanation + ExpansionTile( + title: Text( + "How Silent Payments Work (Sending)", + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.smallMed12( + context, + ).copyWith(fontWeight: FontWeight.bold), + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + "1. Sender gets recipient's Silent Payment address (sp1...)\n" + "2. Sender collects all input public keys (A1, A2, ..., An)\n" + "3. Calculate sum of all input keys: A_sum = A1 + A2 + ... + An\n" + "4. Compute T = SHA256(TaggedHash(lowest_outpoint || A_sum))\n" + "5. Calculate sender partial secret: s = a_sum * T\n" + "6. Extract B_scan from recipient address\n" + "7. Calculate shared secret: S = B_scan * s\n" + "8. For each output i, calculate outputTweak = TaggedHash(S || i)\n" + "9. Generate output address: B_spend + outputTweak\n" + "10. Send payment to derived address", + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildScanSection(bool isDesktop, StackColors colors) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Scan for Received Silent Payments", + style: + isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.titleBold12(context), + ), + const SizedBox(height: 16), + + // Block height + Text( + "Block Height to Scan:", + style: + isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.smallMed12(context), + ), + const SizedBox(height: 8), + TextField( + controller: _blockHeightController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: "800000", + filled: true, + fillColor: colors.textFieldDefaultBG, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + ), + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.smallMed12(context), + ), + + const SizedBox(height: 24), + + // Scan button + Center( + child: PrimaryButton( + width: 150, + label: "Scan", + onPressed: _scanForSilentPayments, + ), + ), + + const SizedBox(height: 16), + + // Detected outputs + if (_detectedOutputs.isNotEmpty) ...[ + Text( + "Detected Silent Payment Outputs:", + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.smallMed12( + context, + ).copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.textFieldDefaultBG, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + _detectedOutputs.map((output) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + child: Text( + output, + style: + isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.smallMed12(context), + ), + ), + SecondaryButton( + label: "Copy", + buttonHeight: ButtonHeight.s, + iconSpacing: 4, + icon: CopyIcon( + width: 10, + height: 10, + color: + Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: output), + ); + if (context.mounted) { + await showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ); + } + }, + ), + ], + ), + ); + }).toList(), + ), + ), + ], + + const SizedBox(height: 16), + + // Receive process explanation + ExpansionTile( + title: Text( + "How Silent Payments Work (Receiving)", + style: + isDesktop + ? STextStyles.desktopTextSmall( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.smallMed12( + context, + ).copyWith(fontWeight: FontWeight.bold), + ), + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + "1. Recipient scans each transaction in new blocks\n" + "2. For each transaction, compute A_sum from input public keys\n" + "3. Calculate T = SHA256(TaggedHash(lowest_outpoint || A_sum))\n" + "4. Calculate receiver partial secret: r = T * b_scan\n" + "5. Calculate shared secret: S = A_sum * r\n" + "6. For each output i, calculate outputTweak = TaggedHash(S || i)\n" + "7. Derive expected output: B_spend + outputTweak\n" + "8. Check if any transaction outputs match expected addresses\n" + "9. If match found, calculate private key: b_spend + outputTweak", + style: + isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDebugSection(bool isDesktop, StackColors colors) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Debug Information", + style: + isDesktop + ? STextStyles.desktopTextMedium( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.titleBold12(context), + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors.textFieldDefaultBG, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + _debugInfo.entries.map((entry) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 150, + child: Text( + "${entry.key}:", + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ).copyWith(fontWeight: FontWeight.bold) + : STextStyles.smallMed12( + context, + ).copyWith(fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: Text( + entry.value, + style: + isDesktop + ? STextStyles.desktopTextExtraSmall( + context, + ) + : STextStyles.smallMed12(context), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } +} 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..a83d5326b 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 @@ -18,6 +18,8 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; import 'package:tuple/tuple.dart'; +import 'package:silent_payments/silent_payments.dart'; + import '../../../../models/isar/models/isar_models.dart'; import '../../../../models/keys/view_only_wallet_data.dart'; import '../../../../notifications/show_flush_bar.dart'; @@ -198,6 +200,45 @@ class _DesktopReceiveState extends ConsumerState { } } + Future generateSilentPaymentAddress() async { + final wallet = ref.read(pWallets).getWallet(walletId); + if (wallet is BitcoinWallet) { + final rootNode = await wallet.getRootHDNode(); + final owner = SilentPaymentOwner.fromBip32(rootNode); + + // Translate the wallet's network to what the Silent Payment library expects + final network = switch (wallet.info.coin.network) { + CryptoCurrencyNetwork.main => 'BitcoinNetwork.mainnet', + CryptoCurrencyNetwork.test => 'BitcoinNetwork.testnet', + CryptoCurrencyNetwork.test4 => 'BitcoinNetwork.testnet', + _ => null + }; + + final address = Address( + walletId: walletId, + value: owner.toString(network: network), + publicKey: [], // Could store both public keys if needed + derivationIndex: 0, // Could keep track of this if generating multiple + derivationPath: null, // BIP-352 uses two paths, so this may not apply directly + type: AddressType.bip352, + subType: AddressSubType.nonWallet, + ); + + final isar = ref.read(mainDBProvider).isar; + await isar.writeTxn(() async { + await isar.addresses.put(address); + }); + + if (mounted) { + Navigator.of(context, rootNavigator: true).pop(); + + setState(() { + _addressMap[_walletAddressTypes[_currentIndex]] = address.value; + }); + } + } + } + @override void initState() { walletId = widget.walletId; @@ -231,6 +272,7 @@ class _DesktopReceiveState extends ConsumerState { if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) { _walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh); + _walletAddressTypes.add(AddressType.bip352); } _addressMap[_walletAddressTypes[_currentIndex]] = @@ -238,6 +280,12 @@ class _DesktopReceiveState extends ConsumerState { if (showMultiType) { for (final type in _walletAddressTypes) { + // For Silent Payment addresses, we'll use a different approach + if (type == AddressType.bip352) { + // Check if a Silent Payment address exists + _checkAndGenerateSilentPaymentAddress(); + continue; // Skip the regular subscription for this type + } _addressSubMap[type] = ref .read(mainDBProvider) .isar @@ -265,6 +313,33 @@ class _DesktopReceiveState extends ConsumerState { super.initState(); } + // Separate method to check for and generate Silent Payment address if needed + Future _checkAndGenerateSilentPaymentAddress() async { + if (_walletAddressTypes.contains(AddressType.bip352)) { + final isar = ref.read(mainDBProvider).isar; + + // Check if a Silent Payment address already exists + final existingAddress = await isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .typeEqualTo(AddressType.bip352) + .findFirst(); + + if (existingAddress != null) { + // Address exists, update the map + if (mounted) { + setState(() { + _addressMap[AddressType.bip352] = existingAddress.value; + }); + } + } else { + // No address exists, generate one + await generateSilentPaymentAddress(); + } + } + } + @override void dispose() { for (final subscription in _addressSubMap.values) { @@ -290,7 +365,8 @@ class _DesktopReceiveState extends ConsumerState { final bool canGen; if (wallet is ViewOnlyOptionInterface && wallet.isViewOnly && - wallet.viewOnlyType == ViewOnlyWalletType.addressOnly) { + wallet.viewOnlyType == ViewOnlyWalletType.addressOnly || + _walletAddressTypes[_currentIndex] == AddressType.bip352) { canGen = false; } else { canGen = (wallet is MultiAddressInterface || supportsSpark); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index 78a36d8d7..2c1c51536 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 @@ -21,6 +21,7 @@ import '../../../../pages/monkey/monkey_view.dart'; import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; +import '../../../../pages/silent_payments/silent_payments_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; import '../../../../providers/global/paynym_api_provider.dart'; import '../../../../providers/providers.dart'; @@ -99,6 +100,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { onFusionPressed: _onFusionPressed, onChurnPressed: _onChurnPressed, onNamesPressed: _onNamesPressed, + onSilentPaymentsPressed: _onSilentPaymentsPressed, ), ); } @@ -371,6 +373,13 @@ class _DesktopWalletFeaturesState extends ConsumerState { ).pushNamed(NamecoinNamesHomeView.routeName, arguments: widget.walletId); } + void _onSilentPaymentsPressed() { + Navigator.of(context, rootNavigator: true).pop(); + Navigator.of( + context, + ).pushNamed(SilentPaymentsView.routeName, arguments: widget.walletId); + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 1ee0447b3..d39083ba3 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 @@ -32,6 +32,7 @@ import '../../../../../utilities/util.dart'; import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../../../wallets/wallet/impl/bitcoin_wallet.dart'; import '../../../../../wallets/wallet/impl/firo_wallet.dart'; import '../../../../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; @@ -67,6 +68,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { required this.onFusionPressed, required this.onChurnPressed, required this.onNamesPressed, + required this.onSilentPaymentsPressed, }); final String walletId; @@ -82,6 +84,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { final VoidCallback? onFusionPressed; final VoidCallback? onChurnPressed; final VoidCallback? onNamesPressed; + final VoidCallback? onSilentPaymentsPressed; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -419,6 +422,7 @@ class _MoreFeaturesDialogState extends ConsumerState { // iconAsset: Assets.svg.whirlPool, // onPressed: () => widget.onWhirlpoolPressed?.call(), // ), + if (wallet is CoinControlInterface && coinControlPrefEnabled) _MoreFeaturesItem( label: "Coin control", @@ -457,6 +461,13 @@ class _MoreFeaturesDialogState extends ConsumerState { iconAsset: Assets.svg.robotHead, onPressed: () async => widget.onPaynymPressed?.call(), ), + if (!isViewOnly && wallet is BitcoinWallet) + _MoreFeaturesItem( + label: "Silent Payments", + detail: "Increased address privacy using BIP352", + iconAsset: Assets.svg.ellipsis, + onPressed: () async => widget.onSilentPaymentsPressed?.call(), + ), if (wallet is OrdinalsInterface) _MoreFeaturesItem( label: "Ordinals", diff --git a/lib/providers/silent_payment/silent_payment_provider.dart b/lib/providers/silent_payment/silent_payment_provider.dart new file mode 100644 index 000000000..64fd13dc2 --- /dev/null +++ b/lib/providers/silent_payment/silent_payment_provider.dart @@ -0,0 +1,90 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; + +import '../../models/isar/models/silent_payments/silent_payment_config.dart'; +import '../../models/isar/models/silent_payments/silent_payment_metadata.dart'; +import '../../providers/db/main_db_provider.dart'; +import '../../wallets/isar/providers/util/watcher.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; + +/// Base provider that watches the SilentPaymentConfig object in the database +final _silentPaymentConfigProvider = + ChangeNotifierProvider.family((ref, walletId) { + final isar = ref.watch(mainDBProvider).isar; + final collection = isar.silentPaymentConfig; + + // Try to find existing config + var config = collection.where().walletIdEqualTo(walletId).findFirstSync(); + + // If no config exists, create and save a new one + if (config == null) { + config = SilentPaymentConfig(walletId: walletId); + isar.writeTxnSync(() { + isar.silentPaymentConfig.putSync(config!); + }); + } + + // Create a watcher for this config + final watcher = Watcher(config, collection: collection); + + // Clean up when provider is disposed + ref.onDispose(() => watcher.dispose()); + + return watcher; + }); + +/// Provider for the entire SilentPaymentConfig object +final pSilentPaymentConfig = Provider.family(( + ref, + walletId, +) { + return ref.watch(_silentPaymentConfigProvider(walletId)).value + as SilentPaymentConfig; +}); + +/// Provider for just the enabled state +final pSilentPaymentEnabled = Provider.family((ref, walletId) { + return ref.watch( + _silentPaymentConfigProvider( + walletId, + ).select((value) => (value.value as SilentPaymentConfig).isEnabled), + ); +}); + +/// Provider for the last scanned height +final pSilentPaymentLastScannedHeight = Provider.family(( + ref, + walletId, +) { + return ref.watch( + _silentPaymentConfigProvider( + walletId, + ).select((value) => (value.value as SilentPaymentConfig).lastScannedHeight), + ); +}); + +/// Provider for the label map +final pSilentPaymentLabelMap = Provider.family?, String>(( + ref, + walletId, +) { + return ref.watch( + _silentPaymentConfigProvider( + walletId, + ).select((value) => (value.value as SilentPaymentConfig).labelMap), + ); +}); + +/// Provider to determine if scanning is needed (compares with wallet height) +final pSilentPaymentScanNeeded = Provider.family((ref, walletId) { + final config = ref.watch(pSilentPaymentConfig(walletId)); + + // Import the wallet chain height provider where needed + try { + final walletHeight = ref.watch(pWalletChainHeight(walletId)); + return config.isEnabled && walletHeight > config.lastScannedHeight; + } catch (_) { + // Handle case where pWalletChainHeight isn't imported + return false; + } +}); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 6695a53b8..49fdeddf1 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -147,6 +147,7 @@ import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_setting import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/spark_info.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; +import 'pages/silent_payments/silent_payments_view.dart'; import 'pages/special/firo_rescan_recovery_error_dialog.dart'; import 'pages/stack_privacy_calls.dart'; import 'pages/token_view/my_tokens_view.dart'; @@ -253,12 +254,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePinView( - popOnSuccess: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePinView(popOnSuccess: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -285,14 +282,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseCoinView( - title: args.item1, - coinAdditional: args.item2, - nextRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ChooseCoinView( + title: args.item1, + coinAdditional: args.item2, + nextRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -301,12 +297,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageExplorerView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ManageExplorerView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -315,12 +307,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FiroRescanRecoveryErrorView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FiroRescanRecoveryErrorView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -343,23 +331,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditWalletTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditWalletTokensView( - walletId: args.item1, - contractsToMarkSelected: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditWalletTokensView( + walletId: args.item1, + contractsToMarkSelected: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -368,12 +351,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopTokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopTokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -382,12 +361,8 @@ class RouteGenerator { if (args is EthTokenEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectWalletForTokenView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SelectWalletForTokenView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -396,21 +371,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const AddCustomTokenView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletsOverview.routeName: if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletsOverview( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletsOverview(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -419,13 +388,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenContractDetailsView( - contractAddress: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenContractDetailsView( + contractAddress: args.item1, + walletId: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -434,13 +402,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SingleFieldEditView( - initialValue: args.item1, - label: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SingleFieldEditView( + initialValue: args.item1, + label: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -449,66 +416,50 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MonkeyView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MonkeyView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case CreateNewFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateNewFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CreateNewFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreFrostMsWalletView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFrostMsWalletView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreFrostMsWalletView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case SelectNewFrostImportTypeView.routeName: - if (args is ({ - String walletName, - FrostCurrency frostCurrency, - })) { + if (args is ({String walletName, FrostCurrency frostCurrency})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SelectNewFrostImportTypeView( - walletName: args.walletName, - frostCurrency: args.frostCurrency, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SelectNewFrostImportTypeView( + walletName: args.walletName, + frostCurrency: args.frostCurrency, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -517,21 +468,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const FrostStepScaffold(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case FrostMSWalletOptionsView.routeName: if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostMSWalletOptionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostMSWalletOptionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -540,12 +485,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostParticipantsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FrostParticipantsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -554,12 +495,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => InitiateResharingView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => InitiateResharingView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -568,31 +505,23 @@ class RouteGenerator { if (args is ({String walletId, Map resharers})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CompleteReshareConfigView( - walletId: args.walletId, - resharers: args.resharers, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CompleteReshareConfigView( + walletId: args.walletId, + resharers: args.resharers, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FrostSendView.routeName: - if (args is ({ - String walletId, - CryptoCurrency coin, - })) { + if (args is ({String walletId, CryptoCurrency coin})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FrostSendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => FrostSendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -616,27 +545,22 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView(walletId: args.item1, type: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple4?>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinControlView( - walletId: args.item1, - type: args.item2, - requestedTotal: args.item3, - selectedUTXOs: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => CoinControlView( + walletId: args.item1, + type: args.item2, + requestedTotal: args.item3, + selectedUTXOs: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -645,12 +569,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => OrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -659,12 +579,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopOrdinalsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -673,13 +589,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => OrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -688,13 +603,12 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopOrdinalDetailsView( - walletId: args.walletId, - ordinal: args.ordinal, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DesktopOrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -710,13 +624,10 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => UtxoDetailsView( - walletId: args.item2, - utxoId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + UtxoDetailsView(walletId: args.item2, utxoId: args.item1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -725,13 +636,8 @@ class RouteGenerator { if (args is (Id, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameDetailsView( - walletId: args.$2, - utxoId: args.$1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NameDetailsView(walletId: args.$2, utxoId: args.$1), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -740,12 +646,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymClaimView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymClaimView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -754,12 +656,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => PaynymHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => PaynymHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -768,12 +666,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewPaynymFollowView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewPaynymFollowView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -782,12 +676,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -796,12 +686,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NamecoinNamesHomeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NamecoinNamesHomeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -810,13 +696,10 @@ class RouteGenerator { if (args is ({String walletId, UTXO utxo})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ManageDomainView( - walletId: args.walletId, - utxo: args.utxo, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => + ManageDomainView(walletId: args.walletId, utxo: args.utxo), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -825,12 +708,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => FusionProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -839,12 +718,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -853,12 +728,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChurningProgressView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChurningProgressView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -867,12 +738,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCashFusionView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCashFusionView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -881,12 +748,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopChurningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopChurningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -902,12 +765,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressBookView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddressBookView(coin: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -1011,13 +870,8 @@ class RouteGenerator { if (args is (String, ({List xpubs, String fingerprint}))) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => XPubView( - walletId: args.$1, - xpubData: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => XPubView(walletId: args.$1, xpubData: args.$2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1026,12 +880,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChangeRepresentativeView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChangeRepresentativeView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1117,12 +967,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreFromEncryptedStringView( - encrypted: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RestoreFromEncryptedStringView(encrypted: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1138,12 +984,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditCoinUnitsView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditCoinUnitsView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1173,12 +1015,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CoinNodesView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CoinNodesView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1187,14 +1025,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NodeDetailsView( - coin: args.item1, - nodeId: args.item2, - popRouteName: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NodeDetailsView( + coin: args.item1, + nodeId: args.item2, + popRouteName: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1203,13 +1040,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditNoteView( - txid: args.item1, - walletId: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditNoteView(txid: args.item1, walletId: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1218,12 +1051,8 @@ class RouteGenerator { if (args is int) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditAddressLabelView( - addressLabelId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditAddressLabelView(addressLabelId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1232,13 +1061,9 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditTradeNoteView( - tradeId: args.item1, - note: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditTradeNoteView(tradeId: args.item1, note: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1248,15 +1073,14 @@ class RouteGenerator { is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddEditNodeView( - viewType: args.item1, - coin: args.item2, - nodeId: args.item3, - routeOnSuccessOrDelete: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddEditNodeView( + viewType: args.item1, + coin: args.item2, + nodeId: args.item3, + routeOnSuccessOrDelete: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1265,12 +1089,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ContactDetailsView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ContactDetailsView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1279,12 +1099,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddNewContactAddressView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AddNewContactAddressView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1293,12 +1109,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactNameEmojiView( - contactId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditContactNameEmojiView(contactId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1307,13 +1119,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditContactAddressView( - contactId: args.item1, - addressEntry: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => EditContactAddressView( + contactId: args.item1, + addressEntry: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1322,23 +1133,20 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => const SystemBrightnessThemeSelectionView(), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); case WalletNetworkSettingsView.routeName: if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletNetworkSettingsView( - walletId: args.item1, - initialSyncStatus: args.item2, - initialNodeStatus: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletNetworkSettingsView( + walletId: args.item1, + initialSyncStatus: args.item2, + initialNodeStatus: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1347,91 +1155,88 @@ class RouteGenerator { if (args is ({String walletId, List mnemonic})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonic, - ({ - String myName, - String config, - String keys, - ({String config, String keys})? prevGen, - })? frostWalletData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - frostWalletData: args.frostWalletData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + frostWalletData: args.frostWalletData, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonic, - KeyDataInterface? keyData, - ({ - String myName, - String config, - String keys, - ({String config, String keys})? prevGen, - })? frostWalletData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletBackupView( - walletId: args.walletId, - mnemonic: args.mnemonic, - frostWalletData: args.frostWalletData, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonic, + KeyDataInterface? keyData, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => WalletBackupView( + walletId: args.walletId, + mnemonic: args.mnemonic, + frostWalletData: args.frostWalletData, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case MobileKeyDataView.routeName: - if (args is ({ - String walletId, - KeyDataInterface keyData, - })) { + if (args is ({String walletId, KeyDataInterface keyData})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MobileKeyDataView( - walletId: args.walletId, - keyData: args.keyData, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => MobileKeyDataView( + walletId: args.walletId, + keyData: args.keyData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1440,12 +1245,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsWalletSettingsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletSettingsWalletSettingsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1454,12 +1255,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RenameWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => RenameWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1468,12 +1265,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletWarningView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeleteWalletWarningView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1482,12 +1275,8 @@ class RouteGenerator { if (args is AddWalletListEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreateOrRestoreWalletView( - entity: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreateOrRestoreWalletView(entity: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1496,13 +1285,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NameYourWalletView( - addWalletType: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NameYourWalletView( + addWalletType: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1511,13 +1299,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseWarningView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseWarningView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1526,13 +1313,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1541,13 +1327,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletOptionsView( - walletName: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletOptionsView( + walletName: args.item1, + coin: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1557,39 +1342,38 @@ class RouteGenerator { is Tuple6) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreWalletView( - walletName: args.item1, - coin: args.item2, - seedWordsLength: args.item3, - restoreFromDate: args.item4, - mnemonicPassphrase: args.item5, - enableLelantusScanning: args.item6 ?? false, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => RestoreWalletView( + walletName: args.item1, + coin: args.item2, + seedWordsLength: args.item3, + restoreFromDate: args.item4, + mnemonicPassphrase: args.item5, + enableLelantusScanning: args.item6 ?? false, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case RestoreViewOnlyWalletView.routeName: - if (args is ({ - String walletName, - CryptoCurrency coin, - DateTime? restoreFromDate, - bool enableLelantusScanning, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => RestoreViewOnlyWalletView( - walletName: args.walletName, - coin: args.coin, - restoreFromDate: args.restoreFromDate, - enableLelantusScanning: args.enableLelantusScanning, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + String walletName, + CryptoCurrency coin, + DateTime? restoreFromDate, + bool enableLelantusScanning, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => RestoreViewOnlyWalletView( + walletName: args.walletName, + coin: args.coin, + restoreFromDate: args.restoreFromDate, + enableLelantusScanning: args.enableLelantusScanning, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1598,13 +1382,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NewWalletRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => NewWalletRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1613,13 +1396,12 @@ class RouteGenerator { if (args is Tuple2>) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => VerifyRecoveryPhraseView( - wallet: args.item1, - mnemonic: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => VerifyRecoveryPhraseView( + wallet: args.item1, + mnemonic: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1634,12 +1416,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1648,54 +1426,49 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionDetailsView( - transaction: args.item1, - coin: args.item2, - walletId: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionDetailsView( + transaction: args.item1, + coin: args.item2, + walletId: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case TransactionV2DetailsView.routeName: - if (args is ({ - TransactionV2 tx, - CryptoCurrency coin, - String walletId - })) { + if (args + is ({TransactionV2 tx, CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionV2DetailsView( - transaction: args.tx, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TransactionV2DetailsView( + transaction: args.tx, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case FusionGroupDetailsView.routeName: - if (args is ({ - List transactions, - CryptoCurrency coin, - String walletId - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => FusionGroupDetailsView( - transactions: args.transactions, - coin: args.coin, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + if (args + is ({ + List transactions, + CryptoCurrency coin, + String walletId, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => FusionGroupDetailsView( + transactions: args.transactions, + coin: args.coin, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1704,12 +1477,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1718,24 +1487,19 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => AllTransactionsV2View(walletId: args), + settings: RouteSettings(name: settings.name), ); } if (args is ({String walletId, String contractAddress})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AllTransactionsV2View( - walletId: args.walletId, - contractAddress: args.contractAddress, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AllTransactionsV2View( + walletId: args.walletId, + contractAddress: args.contractAddress, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1744,12 +1508,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TransactionSearchFilterView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TransactionSearchFilterView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1758,23 +1518,18 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ReceiveView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ReceiveView( - walletId: args.item1, - tokenContract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ReceiveView( + walletId: args.item1, + tokenContract: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1783,12 +1538,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => WalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1797,13 +1548,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => AddressDetailsView( - walletId: args.item2, - addressId: args.item1, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => AddressDetailsView( + walletId: args.item2, + addressId: args.item1, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1812,49 +1562,37 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.item1, coin: args.item2), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - autoFillData: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + autoFillData: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.item1, - coin: args.item2, - accountLite: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendView( + walletId: args.item1, + coin: args.item2, + accountLite: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } else if (args is ({CryptoCurrency coin, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendView( - walletId: args.walletId, - coin: args.coin, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SendView(walletId: args.walletId, coin: args.coin), + settings: RouteSettings(name: settings.name), ); } @@ -1864,14 +1602,13 @@ class RouteGenerator { if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenSendView( - walletId: args.item1, - coin: args.item2, - tokenContract: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenSendView( + walletId: args.item1, + coin: args.item2, + tokenContract: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1880,14 +1617,13 @@ class RouteGenerator { if (args is (TxData, String, VoidCallback)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmTransactionView( - txData: args.$1, - walletId: args.$2, - onSuccess: args.$3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmTransactionView( + txData: args.$1, + walletId: args.$2, + onSuccess: args.$3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1896,13 +1632,12 @@ class RouteGenerator { if (args is (TxData, String)) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ConfirmNameTransactionView( - txData: args.$1, - walletId: args.$2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => ConfirmNameTransactionView( + txData: args.$1, + walletId: args.$2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1911,40 +1646,38 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple3) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Stack( - children: [ - WalletInitiatedExchangeView( - walletId: args.item1, - coin: args.item2, - contract: args.item3, + builder: + (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + contract: args.item3, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], ), - // ExchangeLoadingOverlayView( - // unawaitedLoad: args.item3, - // ), - ], - ), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1953,30 +1686,30 @@ class RouteGenerator { if (args is String?) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => NotificationsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => NotificationsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); case WalletSettingsView.routeName: - if (args is Tuple4) { + if (args + is Tuple4< + String, + CryptoCurrency, + WalletSyncStatus, + NodeConnectionStatus + >) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => WalletSettingsView( - walletId: args.item1, - coin: args.item2, - initialSyncStatus: args.item3, - initialNodeStatus: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => WalletSettingsView( + walletId: args.item1, + coin: args.item2, + initialSyncStatus: args.item3, + initialNodeStatus: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -1985,34 +1718,34 @@ class RouteGenerator { if (args is ({String walletId, List mnemonicWords})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); - } else if (args is ({ - String walletId, - List mnemonicWords, - ({ - String myName, - String config, - String keys, - ({String config, String keys})? prevGen, - })? frostWalletData, - })) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteWalletRecoveryPhraseView( - mnemonic: args.mnemonicWords, - walletId: args.walletId, - frostWalletData: args.frostWalletData, - ), - settings: RouteSettings( - name: settings.name, - ), + } else if (args + is ({ + String walletId, + List mnemonicWords, + ({ + String myName, + String config, + String keys, + ({String config, String keys})? prevGen, + })? + frostWalletData, + })) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: + (_) => DeleteWalletRecoveryPhraseView( + mnemonic: args.mnemonicWords, + walletId: args.walletId, + frostWalletData: args.frostWalletData, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2021,13 +1754,12 @@ class RouteGenerator { if (args is ({String walletId, ViewOnlyWalletData data})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeleteViewOnlyWalletKeysView( - data: args.data, - walletId: args.walletId, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => DeleteViewOnlyWalletKeysView( + data: args.data, + walletId: args.walletId, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2038,12 +1770,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step1View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step1View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2052,12 +1780,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step2View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step2View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2066,12 +1790,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step3View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step3View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2080,12 +1800,8 @@ class RouteGenerator { if (args is IncompleteExchangeModel) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => Step4View( - model: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => Step4View(model: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2094,15 +1810,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TradeDetailsView( - tradeId: args.item1, - transactionIfSentFromStack: args.item2, - walletId: args.item3, - walletName: args.item4, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TradeDetailsView( + tradeId: args.item1, + transactionIfSentFromStack: args.item2, + walletId: args.item3, + walletName: args.item4, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2111,12 +1826,8 @@ class RouteGenerator { if (args is CryptoCurrency) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => ChooseFromStackView( - coin: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => ChooseFromStackView(coin: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2125,15 +1836,14 @@ class RouteGenerator { if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SendFromView( - coin: args.item1, - amount: args.item2, - trade: args.item4, - address: args.item3, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => SendFromView( + coin: args.item1, + amount: args.item2, + trade: args.item4, + address: args.item3, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2142,13 +1852,12 @@ class RouteGenerator { if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => GenerateUriQrCodeView( - coin: args.item1, - receivingAddress: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => GenerateUriQrCodeView( + coin: args.item1, + receivingAddress: args.item2, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2157,12 +1866,8 @@ class RouteGenerator { if (args is SimplexQuote) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyQuotePreviewView( - quote: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BuyQuotePreviewView(quote: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2172,9 +1877,7 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => LelantusSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2184,9 +1887,7 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => RbfSettingsView(walletId: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2195,12 +1896,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkInfoView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkInfoView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2209,12 +1906,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => EditRefreshHeightView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => EditRefreshHeightView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2223,13 +1916,12 @@ class RouteGenerator { if (args is ({String walletId, String domainName})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyDomainView( - walletId: args.walletId, - domainName: args.domainName, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyDomainView( + walletId: args.walletId, + domainName: args.domainName, + ), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2239,12 +1931,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => CreatePasswordView( - restoreFromSWB: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => CreatePasswordView(restoreFromSWB: args), + settings: RouteSettings(name: settings.name), ); } return getRoute( @@ -2271,12 +1959,8 @@ class RouteGenerator { if (args is bool) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DeletePasswordWarningView( - shouldCreateNew: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DeletePasswordWarningView(shouldCreateNew: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2314,21 +1998,15 @@ class RouteGenerator { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => BuyInWalletView(coin: args), - settings: RouteSettings( - name: settings.name, - ), + settings: RouteSettings(name: settings.name), ); } if (args is Tuple2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BuyInWalletView( - coin: args.item1, - contract: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => BuyInWalletView(coin: args.item1, contract: args.item2), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2365,12 +2043,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2379,12 +2053,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopWalletAddressesView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopWalletAddressesView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2393,12 +2063,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => LelantusCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => LelantusCoinsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2407,12 +2073,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => SparkCoinsView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => SparkCoinsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2421,12 +2083,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => DesktopCoinControlView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => DesktopCoinControlView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2435,12 +2093,8 @@ class RouteGenerator { if (args is TransactionV2) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => BoostTransactionView( - transaction: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => BoostTransactionView(transaction: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2530,27 +2184,27 @@ class RouteGenerator { ); case WalletKeysDesktopPopup.routeName: - if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData - })) { + if (args + is ({ + List mnemonic, + String walletId, + ({String keys, String config})? frostData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, frostData: args.frostData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - ({String keys, String config})? frostData, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + ({String keys, String config})? frostData, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, @@ -2558,24 +2212,21 @@ class RouteGenerator { frostData: args.frostData, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); - } else if (args is ({ - List mnemonic, - String walletId, - KeyDataInterface? keyData, - })) { + } else if (args + is ({ + List mnemonic, + String walletId, + KeyDataInterface? keyData, + })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, keyData: args.keyData, ), - RouteSettings( - name: settings.name, - ), + RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2583,12 +2234,8 @@ class RouteGenerator { case UnlockWalletKeysDesktop.routeName: if (args is String) { return FadePageRoute( - UnlockWalletKeysDesktop( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + UnlockWalletKeysDesktop(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2605,12 +2252,8 @@ class RouteGenerator { case DesktopDeleteWalletDialog.routeName: if (args is String) { return FadePageRoute( - DesktopDeleteWalletDialog( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopDeleteWalletDialog(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2627,12 +2270,8 @@ class RouteGenerator { case DesktopAttentionDeleteWallet.routeName: if (args is String) { return FadePageRoute( - DesktopAttentionDeleteWallet( - walletId: args, - ), - RouteSettings( - name: settings.name, - ), + DesktopAttentionDeleteWallet(walletId: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2649,13 +2288,8 @@ class RouteGenerator { case DeleteWalletKeysPopup.routeName: if (args is Tuple2>) { return FadePageRoute( - DeleteWalletKeysPopup( - walletId: args.item1, - words: args.item2, - ), - RouteSettings( - name: settings.name, - ), + DeleteWalletKeysPopup(walletId: args.item1, words: args.item2), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2672,12 +2306,8 @@ class RouteGenerator { case QRCodeDesktopPopupContent.routeName: if (args is String) { return FadePageRoute( - QRCodeDesktopPopupContent( - value: args, - ), - RouteSettings( - name: settings.name, - ), + QRCodeDesktopPopupContent(value: args), + RouteSettings(name: settings.name), ); // return getRoute( // shouldUseMaterialRoute: useMaterialPageRoute, @@ -2695,12 +2325,8 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MyTokensView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => MyTokensView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2723,23 +2349,28 @@ class RouteGenerator { if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: (_) => TokenView(walletId: args), + settings: RouteSettings(name: settings.name), ); } else if (args is ({String walletId, bool popPrevious})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => TokenView( - walletId: args.walletId, - popPrevious: args.popPrevious, - ), - settings: RouteSettings( - name: settings.name, - ), + builder: + (_) => TokenView( + walletId: args.walletId, + popPrevious: args.popPrevious, + ), + settings: RouteSettings(name: settings.name), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case SilentPaymentsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SilentPaymentsView(walletId: args), + settings: RouteSettings(name: settings.name), ); } return _routeError("${settings.name} invalid args: ${args.toString()}"); @@ -2785,13 +2416,12 @@ class RouteGenerator { final end = Offset.zero; final curve = Curves.easeInOut; - final tween = - Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + final tween = Tween( + begin: begin, + end: end, + ).chain(CurveTween(curve: curve)); - return SlideTransition( - position: animation.drive(tween), - child: child, - ); + return SlideTransition(position: animation.drive(tween), child: child); }, ); } @@ -2835,10 +2465,7 @@ class FadePageRoute extends PageRoute { Animation animation, Animation secondaryAnimation, ) { - return FadeTransition( - opacity: animation, - child: child, - ); + return FadeTransition(opacity: animation, child: child); } @override diff --git a/lib/services/silentium_api.dart b/lib/services/silentium_api.dart new file mode 100644 index 000000000..5042e8ee0 --- /dev/null +++ b/lib/services/silentium_api.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +/// Service for interacting with the Silentium API +class SilentiumApi { + final String baseUrl; + final http.Client _client; + + SilentiumApi({required this.baseUrl, http.Client? client}) + : _client = client ?? http.Client(); + + /// Close the HTTP client when done + void dispose() { + _client.close(); + } + + /// Get the latest block height from the chain tip + Future getLatestBlockHeight() async { + final response = await _client.get( + Uri.parse('$baseUrl/v1/chain/tip'), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + return data['height'] as int; + } else { + throw Exception( + 'Failed to get latest block height: ${response.statusCode}', + ); + } + } + + /// Get scalars for a specific block height + /// Returns a list of scalar hex strings + Future> getScalarsForBlock(int height) async { + final response = await _client.get( + Uri.parse('$baseUrl/v1/block/$height/scalars'), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + return List.from(data['scalars'] as List); + } else if (response.statusCode == 404) { + // Block not found or no scalars available + return []; + } else { + throw Exception('Failed to get scalars: ${response.statusCode}'); + } + } + + /// Ping the API to check if it's available + Future isAvailable() async { + try { + final response = await _client.get( + Uri.parse('$baseUrl/v1/chain/tip'), + headers: {'Content-Type': 'application/json'}, + ); + return response.statusCode == 200; + } catch (e) { + return false; + } + } +} diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 161c68f5f..fbab6da1a 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -564,7 +564,7 @@ abstract class Wallet { ), ); Logging.instance.e( - "Caught exception in refreshWalletData()", + "Caught exception in refresh()", error: e, stackTrace: s, ); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/silent_payment_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/silent_payment_interface.dart new file mode 100644 index 000000000..5c030921f --- /dev/null +++ b/lib/wallets/wallet/wallet_mixin_interfaces/silent_payment_interface.dart @@ -0,0 +1,517 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:isar/isar.dart'; +import 'package:silent_payments/silent_payments.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/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/isar/models/silent_payments/silent_payment_config.dart'; +import '../../../models/isar/models/silent_payments/silent_payment_metadata.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'; +import '../../../utilities/extensions/extensions.dart'; +import '../../../utilities/logger.dart'; +import '../../crypto_currency/crypto_currency.dart'; +import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../isar/models/wallet_info.dart'; +import '../../models/tx_data.dart'; +import '../impl/bitcoin_wallet.dart'; +import '../intermediate/bip39_hd_wallet.dart'; +import 'electrumx_interface.dart'; +import 'view_only_option_interface.dart'; + +/// A mixin that provides Silent Payment capabilities to a wallet +/// +/// This interface focuses on the unique aspects of Silent Payments: +/// 1. Generating Silent Payment addresses +/// 2. Deriving recipient addresses for sending +/// 3. Scanning for received payments +/// +/// It leverages the existing wallet methods for transaction building +/// and blockchain interaction. +mixin SilentPaymentInterface + on ElectrumXInterface { + // Cache for the silent payment owner to avoid repetitive derivation + SilentPaymentOwner? _silentPaymentOwner; + + // Cached labeled addresses + final Map _labeledAddresses = {}; + + /// Whether Silent Payment scanning is enabled for this wallet + Future get isSilentPaymentScanningEnabled async { + final config = await _getSilentPaymentConfig(); + return config?.isEnabled ?? false; + } + + /// Set whether Silent Payment scanning is enabled + Future setSilentPaymentScanningEnabled(bool enabled) async { + final config = await _getSilentPaymentConfig(); + if (config != null) { + await config.updateEnabled(enabled: enabled, isar: mainDB.isar); + } else { + await mainDB.isar.writeTxn(() async { + await mainDB.isar.silentPaymentConfig.put( + SilentPaymentConfig(walletId: walletId, isEnabled: enabled), + ); + }); + } + } + + /// Get the wallet's Silent Payment address + /// + /// Returns the base Silent Payment address for this wallet + Future getSilentPaymentAddress() async { + final owner = await _getSilentPaymentOwner(); + return owner.toString(network: _getNetworkString()); + } + + /// Get the wallet's Silent Payment address with a specific label + /// + /// Labels allow a single Silent Payment address to be shared while + /// still allowing different addresses to be generated for different + /// uses or senders + Future getLabeledSilentPaymentAddress(int label) async { + final owner = await _getSilentPaymentOwner(); + + if (!_labeledAddresses.containsKey(label)) { + _labeledAddresses[label] = owner.toLabeledAddress(label); + } + + return _labeledAddresses[label]!.toString(network: _getNetworkString()); + } + + /// Derive the recipient address(es) for a Silent Payment transaction + /// + /// This can be used with your existing prepareSend method: + /// ``` + /// final selectedUtxos = await wallet.coinSelection(...); + /// final recipientAddress = await wallet.deriveSilentPaymentAddress("sp1...", selectedUtxos); + /// wallet.prepareSend(txData: TxData(recipients: [(address: recipientAddress, amount: amount)])); + /// ``` + Future deriveSilentPaymentAddress( + String silentPaymentAddress, + List selectedUtxos, { + int amount = 0, + }) async { + // Validate the recipient address + if (!SilentPaymentAddress.regex.hasMatch(silentPaymentAddress)) { + throw Exception("Invalid Silent Payment address format"); + } + + // Ensure we have UTXOs to work with + if (selectedUtxos.isEmpty) { + throw Exception("No UTXOs provided for the transaction"); + } + + // Create a destination from the address + final destination = SilentPaymentDestination.fromAddress( + silentPaymentAddress, + amount, + ); + + // Extract outpoints from selected UTXOs + final outpoints = + selectedUtxos + .map((utxo) => coinlib.OutPoint.fromHex(utxo.txid, utxo.vout)) + .toList(); + + // Get private keys for the inputs needed by the Silent Payment builder + final inputPrivKeyInfos = []; + final pubKeys = []; + + for (final utxo in selectedUtxos) { + final address = utxo.address!; + // TODO: convert to 'Address' type + final privkey = await getPrivateKey(address); + final isP2TR = address.startsWith('bc1p') || address.startsWith('tb1p'); + + inputPrivKeyInfos.add( + ECPrivateInfo(privkey, isP2TR, needsTweaking: isP2TR), + ); + + pubKeys.add(privkey.pubkey); + } + + // Create the Silent Payment Builder + final builder = SilentPaymentBuilder( + outpoints: outpoints, + publicKeys: pubKeys, + hrp: info.coin.network == CryptoCurrencyNetwork.main ? 'bc' : 'tb', + ); + + // Create the outputs - this is where the Silent Payment magic happens + final outputMap = builder.createOutputs(inputPrivKeyInfos, [destination]); + + // Extract the first output for the given destination + if (outputMap.isEmpty || + !outputMap.containsKey(silentPaymentAddress) || + outputMap[silentPaymentAddress]!.isEmpty) { + throw Exception("Failed to derive Silent Payment address"); + } + + return outputMap[silentPaymentAddress]!.first.address.toString(); + } + + /// Derive multiple recipient addresses for a batch of Silent Payment recipients + /// + /// This is useful for sending to multiple Silent Payment addresses in one transaction. + Future> deriveSilentPaymentAddresses( + Map silentPaymentAddressesWithAmounts, + List selectedUtxos, + ) async { + if (silentPaymentAddressesWithAmounts.isEmpty) { + return []; + } + + // Ensure we have UTXOs to work with + if (selectedUtxos.isEmpty) { + throw Exception("No UTXOs provided for the transaction"); + } + + // Create destinations from the addresses + final destinations = + silentPaymentAddressesWithAmounts.entries + .map( + (entry) => + SilentPaymentDestination.fromAddress(entry.key, entry.value), + ) + .toList(); + + // Extract outpoints from selected UTXOs + final outpoints = + selectedUtxos + .map((utxo) => coinlib.OutPoint.fromHex(utxo.txid, utxo.vout)) + .toList(); + + // Get private keys for the inputs needed by the Silent Payment builder + final inputPrivKeyInfos = []; + final pubKeys = []; + + for (final utxo in selectedUtxos) { + final address = utxo.address!; + final privkey = await getPrivateKey(address); + final isP2TR = address.startsWith('bc1p') || address.startsWith('tb1p'); + + inputPrivKeyInfos.add( + ECPrivateInfo(privkey, isP2TR, needsTweaking: isP2TR), + ); + + pubKeys.add(privkey.pubkey); + } + + // Create the Silent Payment Builder + final builder = SilentPaymentBuilder( + outpoints: outpoints, + publicKeys: pubKeys, + hrp: info.coin.network == CryptoCurrencyNetwork.main ? 'bc' : 'tb', + ); + + // Create the outputs - this is where the Silent Payment magic happens + final outputMap = builder.createOutputs(inputPrivKeyInfos, destinations); + + // Extract all derived addresses + final derivedAddresses = []; + for (final outputs in outputMap.values) { + for (final output in outputs) { + derivedAddresses.add(output.address.toString()); + } + } + + return derivedAddresses; + } + + /// Scan for Silent Payments in recent transactions + /// + /// This method checks recent blocks for transactions that contain + /// Silent Payments to this wallet. + /// + /// Returns a list of detected outputs with their details + Future>> scanForSilentPayments({ + int? fromHeight, + int? toHeight, + }) async { + final isEnabled = await isSilentPaymentScanningEnabled; + if (!isEnabled) { + return []; + } + + // Get the config to determine last scanned height + final config = await _getSilentPaymentConfig(); + if (config == null) { + return []; + } + + // Get heights to scan + final currentHeight = await chainHeight; + final scanFromHeight = + fromHeight ?? + (config.lastScannedHeight > 0 + ? config.lastScannedHeight + 1 + : currentHeight - 10); + final scanToHeight = toHeight ?? currentHeight; + + final foundOutputs = >[]; + + // Get the Silent Payment owner + final owner = await _getSilentPaymentOwner(); + + // Initialize precomputed labels from the config + final labelMap = config.labelMap; + + // Scan each block in the range + for (int height = scanFromHeight; height <= scanToHeight; height++) { + // Get transactions in the block + final blockHeader = await electrumXClient.getBlockHeadTip(); + + // TODO: Figure out how to get block tx data! + final txids = await getBlockTransactions(blockHeader['id']); + if (txids.isEmpty) continue; + + // Process each transaction + for (final txid in txids) { + final tx = await getTransaction(txid); + if (tx == null) continue; + + // Get inputs and outputs + final inputs = tx['vin'] as List; + final outputs = tx['vout'] as List; + + // Skip transactions with no inputs or outputs + if (inputs.isEmpty || outputs.isEmpty) continue; + + // Collect outpoints and input public keys + final outpoints = []; + final pubKeys = []; + + for (final input in inputs) { + final prevTxid = input['txid']; + final prevVout = input['vout']; + + if (prevTxid == null || prevVout == null) continue; + + outpoints.add(coinlib.OutPoint.fromHex(prevTxid, prevVout)); + + // Extract public key from input + final witnessData = input['txinwitness']; + if (witnessData is List && witnessData.isNotEmpty) { + try { + final pubkeyHex = witnessData.last; + pubKeys.add(coinlib.ECPublicKey.fromHex(pubkeyHex)); + } catch (_) { + // Skip if unable to extract public key + continue; + } + } + } + + // Skip if unable to extract public keys + if (pubKeys.isEmpty) continue; + + // Create SilentPaymentBuilder to scan outputs + final builder = SilentPaymentBuilder( + outpoints: outpoints, + publicKeys: pubKeys, + hrp: info.coin.network == CryptoCurrencyNetwork.main ? 'bc' : 'tb', + ); + + // Convert outputs to format expected by scanner + final outputsToCheck = []; + for (final output in outputs) { + final scriptPubKey = output['scriptPubKey']; + if (scriptPubKey == null) continue; + + final hexScript = scriptPubKey['hex']; + final value = output['value'] ?? 0; + + if (hexScript == null) continue; + + try { + outputsToCheck.add( + coinlib.Output.fromScriptBytes( + BigInt.from(value * 100000000), // Convert BTC to satoshis + hexToBytes(hexScript), + ), + ); + } catch (_) { + continue; + } + } + + // Scan for outputs belonging to this wallet + final scanResults = builder.scanOutputs( + owner, + outputsToCheck, + precomputedLabels: labelMap, + ); + + // Process found outputs + for (final result in scanResults.entries) { + final output = result.value.output; + final label = result.value.label; + + foundOutputs.add({ + 'txid': txid, + 'address': output.address.toString(), + 'amount': output.amount, + 'label': label, + 'derivedPrivateKey': bytesToHex( + owner.b_spend.tweak(hexToBytes(result.value.tweak))!.data, + ), + }); + + // Import the address to the wallet for tracking + await importAddress( + output.address.toString(), + AddressType.p2tr, + isChange: false, + ); + } + } + } + + // Update the last scanned height + if (scanToHeight > config.lastScannedHeight) { + await config.updateLastScannedHeight(height: scanToHeight, isar: mainDB); + } + + return foundOutputs; + } + + /// Add a label for the wallet's Silent Payment address + /// + /// Returns the labeled address + Future addSilentPaymentLabel(int label) async { + final owner = await _getSilentPaymentOwner(); + final labeledAddress = owner.toLabeledAddress(label); + + // Get or create the config + var config = await _getSilentPaymentConfig(); + if (config == null) { + await mainDB.writeTxn(() async { + await mainDB.silentPaymentConfig.put( + SilentPaymentConfig(walletId: walletId), + ); + }); + config = await _getSilentPaymentConfig(); + } + + if (config != null) { + // Generate label data + final generatedLabel = owner.generateLabel(label); + final G = ECPublicKey(ECCurve_secp256k1().G.getEncoded(true)); + + // Add to the config's label map + final labelMap = config.labelMap ?? {}; + labelMap[bytesToHex(tweakMulPublic(G, generatedLabel))] = bytesToHex( + generatedLabel, + ); + + await config.updateLabelMap(newLabelMap: labelMap, isar: mainDB); + } + + // Update cache + _labeledAddresses[label] = labeledAddress; + + return labeledAddress.toString(network: _getNetworkString()); + } + + /// Remove a label from the wallet's Silent Payment address + Future removeSilentPaymentLabel(int label) async { + final config = await _getSilentPaymentConfig(); + if (config == null || config.labelMap == null || config.labelMap!.isEmpty) { + return; + } + + // Generate label data + final owner = await _getSilentPaymentOwner(); + final generatedLabel = owner.generateLabel(label); + final G = ECPublicKey(ECCurve_secp256k1().G.getEncoded(true)); + final labelKey = bytesToHex(tweakMulPublic(G, generatedLabel)); + + // Check if the label exists + if (!config.labelMap!.containsKey(labelKey)) return; + + // Create new map without this label + final updatedMap = Map.from(config.labelMap!); + updatedMap.remove(labelKey); + + // Update the config + await config.updateLabelMap(newLabelMap: updatedMap, isar: mainDB); + + // Update cache + _labeledAddresses.remove(label); + } + + /// Get all Silent Payment labels for this wallet + Future> getSilentPaymentLabels() async { + final config = await _getSilentPaymentConfig(); + if (config == null || config.labelMap == null || config.labelMap!.isEmpty) { + return []; + } + + // We need to regenerate the numeric labels from the stored label data + // This is a brute force approach but works for reasonable numbers of labels + final labelMap = config.labelMap!; + final owner = await _getSilentPaymentOwner(); + final G = ECPublicKey(ECCurve_secp256k1().G.getEncoded(true)); + + final labels = []; + for (int i = 0; i < 1000; i++) { + // Arbitrary limit + final generatedLabel = owner.generateLabel(i); + final labelKey = bytesToHex(tweakMulPublic(G, generatedLabel)); + + if (labelMap.containsKey(labelKey)) { + labels.add(i); + } + } + + return labels; + } + + // Helper method to get the wallet's SilentPaymentOwner + Future _getSilentPaymentOwner() async { + if (_silentPaymentOwner != null) { + return _silentPaymentOwner!; + } + + final rootNode = await getRootHDNode(); + _silentPaymentOwner = SilentPaymentOwner.fromBip32(rootNode); + + return _silentPaymentOwner!; + } + + // Helper method to get the SilentPaymentConfig + Future _getSilentPaymentConfig() async { + return mainDB.isar.silentPaymentConfig + .where() + .walletIdEqualTo(walletId) + .findFirst(); + } + + // Helper method to get the network string in the format expected by SilentPayments + String _getNetworkString() { + switch (info.coin.network) { + case CryptoCurrencyNetwork.main: + return 'BitcoinNetwork.mainnet'; + case CryptoCurrencyNetwork.test: + case CryptoCurrencyNetwork.test4: + return 'BitcoinNetwork.testnet'; + default: + return 'BitcoinNetwork.regtest'; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index b611f6f20..fb062ebec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1762,6 +1762,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + silent_payments: + dependency: "direct main" + description: + path: "." + ref: eb7324f3bcd1c82256b5cdcccaa5aa68809b9af7 + resolved-ref: eb7324f3bcd1c82256b5cdcccaa5aa68809b9af7 + url: "https://github.com/kent-3/silent_payments.git" + source: git + version: "0.1.0" sky_engine: dependency: transitive description: flutter @@ -2327,5 +2336,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"