diff --git a/asset_sources/default_themes/campfire/light.zip b/asset_sources/default_themes/campfire/light.zip index 5dd67bcb1..ca0b9f2f7 100644 Binary files a/asset_sources/default_themes/campfire/light.zip and b/asset_sources/default_themes/campfire/light.zip differ diff --git a/asset_sources/other/ios_launch_image/campfire/LaunchImage.png b/asset_sources/other/ios_launch_image/campfire/LaunchImage.png new file mode 100644 index 000000000..942c30fa7 Binary files /dev/null and b/asset_sources/other/ios_launch_image/campfire/LaunchImage.png differ diff --git a/asset_sources/other/ios_launch_image/campfire/LaunchImage@2x.png b/asset_sources/other/ios_launch_image/campfire/LaunchImage@2x.png new file mode 100644 index 000000000..f5d90b7e2 Binary files /dev/null and b/asset_sources/other/ios_launch_image/campfire/LaunchImage@2x.png differ diff --git a/asset_sources/other/ios_launch_image/campfire/LaunchImage@3x.png b/asset_sources/other/ios_launch_image/campfire/LaunchImage@3x.png new file mode 100644 index 000000000..c7441db43 Binary files /dev/null and b/asset_sources/other/ios_launch_image/campfire/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/asset_sources/other/ios_launch_image/stack_duo/LaunchImage.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to asset_sources/other/ios_launch_image/stack_duo/LaunchImage.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/asset_sources/other/ios_launch_image/stack_duo/LaunchImage@2x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to asset_sources/other/ios_launch_image/stack_duo/LaunchImage@2x.png diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/asset_sources/other/ios_launch_image/stack_duo/LaunchImage@3x.png similarity index 100% rename from ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to asset_sources/other/ios_launch_image/stack_duo/LaunchImage@3x.png diff --git a/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage.png b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage.png new file mode 100644 index 000000000..47903b909 Binary files /dev/null and b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage.png differ diff --git a/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@2x.png b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@2x.png new file mode 100644 index 000000000..863332e3c Binary files /dev/null and b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@2x.png differ diff --git a/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@3x.png b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@3x.png new file mode 100644 index 000000000..17c2de611 Binary files /dev/null and b/asset_sources/other/ios_launch_image/stack_wallet/LaunchImage@3x.png differ diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index 19c76409e..46a7da857 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit 19c76409e55f1bfed58855eb767574604376edb6 +Subproject commit 46a7da857d4113eb3998567b18ac0b33a470f4fd diff --git a/crypto_plugins/frostdart b/crypto_plugins/frostdart index d539de234..2a74a97fb 160000 --- a/crypto_plugins/frostdart +++ b/crypto_plugins/frostdart @@ -1 +1 @@ -Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2 +Subproject commit 2a74a97fb0f0e22a5280b22c010b710cdeec33bb diff --git a/ios/.gitignore b/ios/.gitignore index e96ef602b..cb9c21eb6 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -30,3 +30,6 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +# app specific, handled by scripts +Runner/Assets.xcassets/LaunchImage.imageset/*.png diff --git a/lib/app_config.dart b/lib/app_config.dart index 64ce682fa..5f9a95829 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -15,6 +15,8 @@ abstract class AppConfig { static const prefix = _prefix; static const suffix = _suffix; + static const emptyWalletsMessage = _emptyWalletsMessage; + static String get appDefaultDataDirName => _appDataDirName; static String get shortDescriptionText => _shortDescriptionText; static String get commitHash => _commitHash; diff --git a/lib/main.dart b/lib/main.dart index 09ad6f068..c7b3fccac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -242,7 +242,7 @@ void main(List<String> args) async { // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, // overlays: [SystemUiOverlay.bottom]); - await NotificationApi.init(); + unawaited(NotificationApi.init()); await loadCoinlibFuture; @@ -378,7 +378,8 @@ class _MaterialAppWithThemeState extends ConsumerState<MaterialAppWithTheme> // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet // unawaited(_nodeService.updateCommunityNodes()); - if (AppConfig.hasFeature(AppFeature.swap)) { + if (AppConfig.hasFeature(AppFeature.swap) && + ref.read(prefsChangeNotifierProvider).enableExchange) { await ExchangeDataLoadingService.instance.initDB(); // run without awaiting if (ref.read(prefsChangeNotifierProvider).externalCalls && diff --git a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart index 4f4c123c7..465dd3b13 100644 --- a/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart +++ b/lib/models/isar/models/blockchain_data/v2/transaction_v2.dart @@ -87,7 +87,10 @@ class TransactionV2 { ); } + @ignore int? get size => _getFromOtherData(key: TxV2OdKeys.size) as int?; + + @ignore int? get vSize => _getFromOtherData(key: TxV2OdKeys.vSize) as int?; bool get isEpiccashTransaction => @@ -192,6 +195,9 @@ class TransactionV2 { required int currentChainHeight, required int minConfirms, }) { + String prettyConfirms() => + "(${getConfirmations(currentChainHeight)}/$minConfirms)"; + if (subType == TransactionSubType.cashFusion || subType == TransactionSubType.mint || (subType == TransactionSubType.sparkMint && @@ -199,7 +205,7 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms)) { return "Anonymized"; } else { - return "Anonymizing"; + return "Anonymizing ${prettyConfirms()}"; } } @@ -219,7 +225,7 @@ class TransactionV2 { } else if ((numberOfMessages ?? 0) > 1) { return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) } else { - return "Receiving"; + return "Receiving ${prettyConfirms()}"; } } } else if (type == TransactionType.outgoing) { @@ -231,7 +237,7 @@ class TransactionV2 { } else if ((numberOfMessages ?? 0) > 1) { return "Sending (waiting for confirmations)"; } else { - return "Sending"; + return "Sending ${prettyConfirms()}"; } } } @@ -244,16 +250,20 @@ class TransactionV2 { if (isConfirmed(currentChainHeight, minConfirms)) { return "Received"; } else { - return "Receiving"; + return "Receiving ${prettyConfirms()}"; } } else if (type == TransactionType.outgoing) { if (isConfirmed(currentChainHeight, minConfirms)) { return "Sent"; } else { - return "Sending"; + return "Sending ${prettyConfirms()}"; } } else if (type == TransactionType.sentToSelf) { - return "Sent to self"; + if (isConfirmed(currentChainHeight, minConfirms)) { + return "Sent to self"; + } else { + return "Sent to self ${prettyConfirms()}"; + } } else { return type.name; } diff --git a/lib/models/isar/models/sent_to_address.dart b/lib/models/isar/models/sent_to_address.dart new file mode 100644 index 000000000..f18b18a08 --- /dev/null +++ b/lib/models/isar/models/sent_to_address.dart @@ -0,0 +1,35 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:isar/isar.dart'; + +part 'sent_to_address.g.dart'; + +@Collection() +class SentToAddress { + SentToAddress({ + required this.walletId, + required this.txid, + required this.value, + this.label = "", + }); + + Id id = Isar.autoIncrement; + + @Index() + late final String walletId; + + @Index(unique: true, composite: [CompositeIndex("walletId")]) + late final String txid; + + late final String value; + + late final String label; +} diff --git a/lib/models/isar/models/sent_to_address.g.dart b/lib/models/isar/models/sent_to_address.g.dart new file mode 100644 index 000000000..304d2c5fc --- /dev/null +++ b/lib/models/isar/models/sent_to_address.g.dart @@ -0,0 +1,1248 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sent_to_address.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 + +extension GetSentToAddressCollection on Isar { + IsarCollection<SentToAddress> get sentToAddress => this.collection(); +} + +const SentToAddressSchema = CollectionSchema( + name: r'SentToAddress', + id: 4845779153260162867, + properties: { + r'label': PropertySchema( + id: 0, + name: r'label', + type: IsarType.string, + ), + r'txid': PropertySchema( + id: 1, + name: r'txid', + type: IsarType.string, + ), + r'value': PropertySchema( + id: 2, + name: r'value', + type: IsarType.string, + ), + r'walletId': PropertySchema( + id: 3, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _sentToAddressEstimateSize, + serialize: _sentToAddressSerialize, + deserialize: _sentToAddressDeserialize, + deserializeProp: _sentToAddressDeserializeProp, + idName: r'id', + indexes: { + r'walletId': IndexSchema( + id: -1783113319798776304, + name: r'walletId', + unique: false, + replace: false, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ), + r'txid_walletId': IndexSchema( + id: -2771771174176035985, + name: r'txid_walletId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'txid', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _sentToAddressGetId, + getLinks: _sentToAddressGetLinks, + attach: _sentToAddressAttach, + version: '3.0.5', +); + +int _sentToAddressEstimateSize( + SentToAddress object, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.label.length * 3; + bytesCount += 3 + object.txid.length * 3; + bytesCount += 3 + object.value.length * 3; + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _sentToAddressSerialize( + SentToAddress object, + IsarWriter writer, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + writer.writeString(offsets[0], object.label); + writer.writeString(offsets[1], object.txid); + writer.writeString(offsets[2], object.value); + writer.writeString(offsets[3], object.walletId); +} + +SentToAddress _sentToAddressDeserialize( + Id id, + IsarReader reader, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + final object = SentToAddress( + label: reader.readStringOrNull(offsets[0]) ?? "", + txid: reader.readString(offsets[1]), + value: reader.readString(offsets[2]), + walletId: reader.readString(offsets[3]), + ); + object.id = id; + return object; +} + +P _sentToAddressDeserializeProp<P>( + IsarReader reader, + int propertyId, + int offset, + Map<Type, List<int>> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset) ?? "") as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readString(offset)) as P; + case 3: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _sentToAddressGetId(SentToAddress object) { + return object.id; +} + +List<IsarLinkBase<dynamic>> _sentToAddressGetLinks(SentToAddress object) { + return []; +} + +void _sentToAddressAttach( + IsarCollection<dynamic> col, Id id, SentToAddress object) { + object.id = id; +} + +extension SentToAddressByIndex on IsarCollection<SentToAddress> { + Future<SentToAddress?> getByTxidWalletId(String txid, String walletId) { + return getByIndex(r'txid_walletId', [txid, walletId]); + } + + SentToAddress? getByTxidWalletIdSync(String txid, String walletId) { + return getByIndexSync(r'txid_walletId', [txid, walletId]); + } + + Future<bool> deleteByTxidWalletId(String txid, String walletId) { + return deleteByIndex(r'txid_walletId', [txid, walletId]); + } + + bool deleteByTxidWalletIdSync(String txid, String walletId) { + return deleteByIndexSync(r'txid_walletId', [txid, walletId]); + } + + Future<List<SentToAddress?>> getAllByTxidWalletId( + List<String> txidValues, List<String> walletIdValues) { + final len = txidValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = <List<dynamic>>[]; + for (var i = 0; i < len; i++) { + values.add([txidValues[i], walletIdValues[i]]); + } + + return getAllByIndex(r'txid_walletId', values); + } + + List<SentToAddress?> getAllByTxidWalletIdSync( + List<String> txidValues, List<String> walletIdValues) { + final len = txidValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = <List<dynamic>>[]; + for (var i = 0; i < len; i++) { + values.add([txidValues[i], walletIdValues[i]]); + } + + return getAllByIndexSync(r'txid_walletId', values); + } + + Future<int> deleteAllByTxidWalletId( + List<String> txidValues, List<String> walletIdValues) { + final len = txidValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = <List<dynamic>>[]; + for (var i = 0; i < len; i++) { + values.add([txidValues[i], walletIdValues[i]]); + } + + return deleteAllByIndex(r'txid_walletId', values); + } + + int deleteAllByTxidWalletIdSync( + List<String> txidValues, List<String> walletIdValues) { + final len = txidValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = <List<dynamic>>[]; + for (var i = 0; i < len; i++) { + values.add([txidValues[i], walletIdValues[i]]); + } + + return deleteAllByIndexSync(r'txid_walletId', values); + } + + Future<Id> putByTxidWalletId(SentToAddress object) { + return putByIndex(r'txid_walletId', object); + } + + Id putByTxidWalletIdSync(SentToAddress object, {bool saveLinks = true}) { + return putByIndexSync(r'txid_walletId', object, saveLinks: saveLinks); + } + + Future<List<Id>> putAllByTxidWalletId(List<SentToAddress> objects) { + return putAllByIndex(r'txid_walletId', objects); + } + + List<Id> putAllByTxidWalletIdSync(List<SentToAddress> objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'txid_walletId', objects, saveLinks: saveLinks); + } +} + +extension SentToAddressQueryWhereSort + on QueryBuilder<SentToAddress, SentToAddress, QWhere> { + QueryBuilder<SentToAddress, SentToAddress, QAfterWhere> anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SentToAddressQueryWhere + on QueryBuilder<SentToAddress, SentToAddress, QWhereClause> { + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> idEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> 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<SentToAddress, SentToAddress, QAfterWhereClause> idGreaterThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> idLessThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> 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<SentToAddress, SentToAddress, QAfterWhereClause> walletIdEqualTo( + String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId', + value: [walletId], + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> + 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, + )); + } + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> + txidEqualToAnyWalletId(String txid) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'txid_walletId', + value: [txid], + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> + txidNotEqualToAnyWalletId(String txid) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [], + upper: [txid], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [], + upper: [txid], + includeUpper: false, + )); + } + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> + txidWalletIdEqualTo(String txid, String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'txid_walletId', + value: [txid, walletId], + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterWhereClause> + txidEqualToWalletIdNotEqualTo(String txid, String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid], + upper: [txid, walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid, walletId], + includeLower: false, + upper: [txid], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid, walletId], + includeLower: false, + upper: [txid], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'txid_walletId', + lower: [txid], + upper: [txid, walletId], + includeUpper: false, + )); + } + }); + } +} + +extension SentToAddressQueryFilter + on QueryBuilder<SentToAddress, SentToAddress, QFilterCondition> { + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> idEqualTo( + Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> 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<SentToAddress, SentToAddress, QAfterFilterCondition> + labelEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + labelStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + labelEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + labelContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'label', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + labelMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'label', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + labelIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'label', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + labelIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'label', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> txidEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> txidBetween( + 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'txid', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> txidMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'txid', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txid', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + txidIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'txid', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueBetween( + 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'value', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'value', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + valueIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'value', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + 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<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterFilterCondition> + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension SentToAddressQueryObject + on QueryBuilder<SentToAddress, SentToAddress, QFilterCondition> {} + +extension SentToAddressQueryLinks + on QueryBuilder<SentToAddress, SentToAddress, QFilterCondition> {} + +extension SentToAddressQuerySortBy + on QueryBuilder<SentToAddress, SentToAddress, QSortBy> { + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByLabel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByLabelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByTxid() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByTxidDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> + sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SentToAddressQuerySortThenBy + on QueryBuilder<SentToAddress, SentToAddress, QSortThenBy> { + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByLabel() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByLabelDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'label', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByTxid() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByTxidDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QAfterSortBy> + thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension SentToAddressQueryWhereDistinct + on QueryBuilder<SentToAddress, SentToAddress, QDistinct> { + QueryBuilder<SentToAddress, SentToAddress, QDistinct> distinctByLabel( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'label', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QDistinct> distinctByTxid( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'txid', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QDistinct> distinctByValue( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'value', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<SentToAddress, SentToAddress, QDistinct> distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension SentToAddressQueryProperty + on QueryBuilder<SentToAddress, SentToAddress, QQueryProperty> { + QueryBuilder<SentToAddress, int, QQueryOperations> idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder<SentToAddress, String, QQueryOperations> labelProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'label'); + }); + } + + QueryBuilder<SentToAddress, String, QQueryOperations> txidProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'txid'); + }); + } + + QueryBuilder<SentToAddress, String, QQueryOperations> valueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'value'); + }); + } + + QueryBuilder<SentToAddress, String, QQueryOperations> walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/models/keys/cw_key_data.dart b/lib/models/keys/cw_key_data.dart new file mode 100644 index 000000000..c20c7938c --- /dev/null +++ b/lib/models/keys/cw_key_data.dart @@ -0,0 +1,21 @@ +import 'key_data_interface.dart'; + +class CWKeyData with KeyDataInterface { + CWKeyData({ + required this.walletId, + required String? privateSpendKey, + required String? privateViewKey, + required String? publicSpendKey, + required String? publicViewKey, + }) : keys = List.unmodifiable([ + (label: "Public View Key", key: publicViewKey), + (label: "Private View Key", key: privateViewKey), + (label: "Public Spend Key", key: publicSpendKey), + (label: "Private Spend Key", key: privateSpendKey), + ]); + + @override + final String walletId; + + final List<({String label, String key})> keys; +} diff --git a/lib/models/keys/key_data_interface.dart b/lib/models/keys/key_data_interface.dart new file mode 100644 index 000000000..1b4572f5c --- /dev/null +++ b/lib/models/keys/key_data_interface.dart @@ -0,0 +1,3 @@ +mixin KeyDataInterface { + String get walletId; +} diff --git a/lib/models/keys/xpriv_data.dart b/lib/models/keys/xpriv_data.dart new file mode 100644 index 000000000..cf0959b5d --- /dev/null +++ b/lib/models/keys/xpriv_data.dart @@ -0,0 +1,17 @@ +import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; +import 'key_data_interface.dart'; + +class XPrivData with KeyDataInterface { + XPrivData({ + required this.walletId, + required this.fingerprint, + required List<XPriv> xprivs, + }) : xprivs = List.unmodifiable(xprivs); + + @override + final String walletId; + + final String fingerprint; + + final List<XPriv> xprivs; +} diff --git a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart index 8f2e87fb0..4640e0abb 100644 --- a/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart +++ b/lib/pages/add_wallet_views/frost_ms/new/steps/frost_create_step_5.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../../app_config.dart'; import '../../../../../frost_route_generator.dart'; import '../../../../../notifications/show_flush_bar.dart'; import '../../../../../pages_desktop_specific/desktop_home_view.dart'; @@ -45,7 +46,7 @@ class _FrostCreateStep5State extends ConsumerState<FrostCreateStep5> { static const _warning = "These are your private keys. Please back them up, " "keep them safe and never share it with anyone. Your private keys are the" " only way you can access your funds if you forget PIN, lose your phone, " - "etc. Stack Wallet does not keep nor is able to restore your private keys" + "etc. ${AppConfig.prefix} does not keep nor is able to restore your private keys" "."; late final String seed, recoveryString, serializedKeys, multisigConfig; diff --git a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart index 2c1a248e7..42836e117 100644 --- a/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart +++ b/lib/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart @@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tuple/tuple.dart'; +import '../../../app_config.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; @@ -173,7 +174,7 @@ class _NewWalletRecoveryPhraseWarningViewState "write it down. Keep it safe and never share it with " "anyone. Your recovery phrase is the only way you can" " access your funds if you forget your PIN, lose your" - " phone, etc.\n\nStack Wallet does not keep nor is " + " phone, etc.\n\n${AppConfig.appName} does not keep nor is " "able to restore your recover phrase. Only you have " "access to your wallet.", style: isDesktop @@ -427,7 +428,7 @@ class _NewWalletRecoveryPhraseWarningViewState ), Flexible( child: Text( - "I understand that Stack Wallet does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.", + "I understand that ${AppConfig.appName} does not keep and cannot restore my recovery phrase, and If I lose my recovery phrase, I will not be able to access my funds.", style: isDesktop ? STextStyles.desktopTextMedium( context, diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 9155690cb..2a7a9fcae 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -14,14 +14,9 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tuple/tuple.dart'; + import '../../../notifications/show_flush_bar.dart'; -import '../add_token_view/edit_wallet_tokens_view.dart'; -import '../new_wallet_options/new_wallet_options_view.dart'; -import '../new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; -import '../select_wallet_for_token_view.dart'; -import 'sub_widgets/word_table.dart'; -import 'verify_mnemonic_passphrase_dialog.dart'; -import '../../home_view/home_view.dart'; import '../../../pages_desktop_specific/desktop_home_view.dart'; import '../../../pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; import '../../../providers/db/main_db_provider.dart'; @@ -38,7 +33,13 @@ import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/desktop_app_bar.dart'; import '../../../widgets/desktop/desktop_scaffold.dart'; -import 'package:tuple/tuple.dart'; +import '../../home_view/home_view.dart'; +import '../add_token_view/edit_wallet_tokens_view.dart'; +import '../new_wallet_options/new_wallet_options_view.dart'; +import '../new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; +import '../select_wallet_for_token_view.dart'; +import 'sub_widgets/word_table.dart'; +import 'verify_mnemonic_passphrase_dialog.dart'; final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false); @@ -114,7 +115,8 @@ class _VerifyRecoveryPhraseViewState Future<void> _continue(bool isMatch) async { if (isMatch) { - if (ref.read(pNewWalletOptions.state).state != null) { + if (ref.read(pNewWalletOptions) != null && + ref.read(pNewWalletOptions)!.mnemonicPassphrase.isNotEmpty) { final passphraseVerified = await _verifyMnemonicPassphrase(); if (!passphraseVerified) { diff --git a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart index 2bad87bc6..ba57db69b 100644 --- a/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart +++ b/lib/pages/address_book_views/subviews/new_contact_address_entry_form.dart @@ -75,6 +75,14 @@ class _NewContactAddressEntryFormState addressLabelFocusNode = FocusNode(); addressFocusNode = FocusNode(); coins = [...AppConfig.coins]; + + if (AppConfig.isSingleCoinApp) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + ref.read(addressEntryDataProvider(widget.id)).coin = coins.first; + } + }); + } super.initState(); } @@ -109,7 +117,7 @@ class _NewContactAddressEntryFormState return Column( children: [ - if (isDesktop) + if (isDesktop && !AppConfig.isSingleCoinApp) DropdownButtonHideUnderline( child: DropdownButton2<CryptoCurrency>( hint: Text( @@ -188,7 +196,7 @@ class _NewContactAddressEntryFormState ], ), ), - if (!isDesktop) + if (!isDesktop && !AppConfig.isSingleCoinApp) TextField( autocorrect: Util.isDesktop ? false : true, enableSuggestions: Util.isDesktop ? false : true, @@ -280,9 +288,10 @@ class _NewContactAddressEntryFormState ), ), ), - const SizedBox( - height: 8, - ), + if (!AppConfig.isSingleCoinApp) + const SizedBox( + height: 8, + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 1b1239a27..dfc395492 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -1155,7 +1155,7 @@ class _BuyFormState extends ConsumerState<BuyForm> { ), if (AppConfig.isStackCoin(selectedCrypto?.ticker)) CustomTextButton( - text: "Choose from Stack", + text: "Choose from ${AppConfig.prefix}", onTap: () { try { final coin = AppConfig.getCryptoCurrencyForTicker( diff --git a/lib/pages/cashfusion/fusion_progress_view.dart b/lib/pages/cashfusion/fusion_progress_view.dart index fd2921c7f..e4d787903 100644 --- a/lib/pages/cashfusion/fusion_progress_view.dart +++ b/lib/pages/cashfusion/fusion_progress_view.dart @@ -12,6 +12,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wakelock/wakelock.dart'; import '../../pages_desktop_specific/cashfusion/sub_widgets/fusion_progress.dart'; import '../../providers/cash_fusion/fusion_progress_ui_state_provider.dart'; @@ -84,6 +85,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> { message: "Stopping fusion", ); + await Wakelock.disable(); + return true; } else { return false; @@ -96,6 +99,12 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> { super.initState(); } + @override + void dispose() { + Wakelock.disable(); + super.dispose(); + } + @override Widget build(BuildContext context) { final bool _succeeded = @@ -108,6 +117,8 @@ class _FusionProgressViewState extends ConsumerState<FusionProgressView> { .watch(fusionProgressUIStateProvider(widget.walletId)) .fusionRoundsCompleted; + Wakelock.enable(); + return WillPopScope( onWillPop: () async { return await _requestAndProcessCancel(); diff --git a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart index 111c93240..461dc2935 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_2_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_2_view.dart @@ -194,7 +194,7 @@ class _Step2ViewState extends ConsumerState<Step2View> { ), if (AppConfig.isStackCoin(model.receiveTicker)) CustomTextButton( - text: "Choose from Stack", + text: "Choose from ${AppConfig.prefix}", onTap: () { try { final coin = AppConfig.coins.firstWhere( @@ -480,7 +480,7 @@ class _Step2ViewState extends ConsumerState<Step2View> { ), if (AppConfig.isStackCoin(model.sendTicker)) CustomTextButton( - text: "Choose from Stack", + text: "Choose from ${AppConfig.prefix}", onTap: () { try { final coin = AppConfig.coins.firstWhere( diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 680330753..643768f19 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -14,10 +14,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; + +import '../../app_config.dart'; import '../../models/exchange/response_objects/trade.dart'; -import 'confirm_change_now_send.dart'; -import '../home_view/home_view.dart'; -import '../send_view/sub_widgets/building_transaction_dialog.dart'; import '../../pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import '../../providers/providers.dart'; import '../../route_generator.dart'; @@ -29,12 +28,14 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/logger.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/crypto_currency/crypto_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/models/tx_data.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -43,6 +44,9 @@ import '../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../widgets/expandable.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/stack_dialog.dart'; +import '../home_view/home_view.dart'; +import '../send_view/sub_widgets/building_transaction_dialog.dart'; +import 'confirm_change_now_send.dart'; class SendFromView extends ConsumerStatefulWidget { const SendFromView({ @@ -135,7 +139,7 @@ class _SendFromViewState extends ConsumerState<SendFromView> { left: 32, ), child: Text( - "Send from Stack", + "Send from ${AppConfig.prefix}", style: STextStyles.desktopH3(context), ), ), @@ -269,6 +273,15 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ), ); + // Currently CwBasedInterface wallets (xmr/wow) shouldn't even have + // access to this screen but this is needed to get past an error that + // would occur only to lead to another error which is why xmr/wow wallets + // don't have access to this screen currently + if (wallet is CwBasedInterface) { + await wallet.init(); + await wallet.open(); + } + final time = Future<dynamic>.delayed( const Duration( milliseconds: 2500, @@ -373,7 +386,8 @@ class _SendFromCardState extends ConsumerState<SendFromCard> { ); } } - } catch (e) { + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Error); if (mounted) { // pop building dialog Navigator.of(context).pop(); diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 7c2317768..0416056a7 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -190,6 +190,15 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { final isDesktop = Util.isDesktop; + final showSendFromStackButton = !hasTx && + !["xmr", "monero", "wow", "wownero"] + .contains(trade.payInCurrency.toLowerCase()) && + AppConfig.isStackCoin(trade.payInCurrency) && + (trade.status == "New" || + trade.status == "new" || + trade.status == "waiting" || + trade.status == "Waiting"); + return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -248,23 +257,13 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { children: children, ), ), - if (!hasTx && - AppConfig.isStackCoin(trade.payInCurrency) && - (trade.status == "New" || - trade.status == "new" || - trade.status == "waiting" || - trade.status == "Waiting")) + if (showSendFromStackButton) const SizedBox( height: 32, ), - if (!hasTx && - AppConfig.isStackCoin(trade.payInCurrency) && - (trade.status == "New" || - trade.status == "new" || - trade.status == "waiting" || - trade.status == "Waiting")) + if (showSendFromStackButton) SecondaryButton( - label: "Send from Stack", + label: "Send from ${AppConfig.prefix}", buttonHeight: ButtonHeight.l, onPressed: () { CryptoCurrency coin; @@ -1371,15 +1370,9 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> { const SizedBox( height: 12, ), - if (!isDesktop && - !hasTx && - AppConfig.isStackCoin(trade.payInCurrency) && - (trade.status == "New" || - trade.status == "new" || - trade.status == "waiting" || - trade.status == "Waiting")) + if (!isDesktop && showSendFromStackButton) SecondaryButton( - label: "Send from Stack", + label: "Send from ${AppConfig.prefix}", onPressed: () { CryptoCurrency coin; try { diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 9b6f6eddb..7619a5d34 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -17,6 +17,7 @@ import 'package:flutter_svg/svg.dart'; import '../../app_config.dart'; import '../../providers/global/notifications_provider.dart'; +import '../../providers/global/prefs_provider.dart'; import '../../providers/ui/home_view_index_provider.dart'; import '../../providers/ui/unread_notifications_provider.dart'; import '../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; @@ -172,6 +173,20 @@ class _HomeViewState extends ConsumerState<HomeView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + + // dirty hack + ref.listen( + prefsChangeNotifierProvider.select((value) => value.enableExchange), + (prev, next) { + if (next == false && + mounted && + ref.read(homeViewPageIndexStateProvider) != 0) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(homeViewPageIndexStateProvider.state).state = 0, + ); + } + }); + return WillPopScope( onWillPop: _onWillPop, child: Background( @@ -345,7 +360,8 @@ class _HomeViewState extends ConsumerState<HomeView> { ), body: Column( children: [ - if (_children.length > 1) + if (_children.length > 1 && + ref.watch(prefsChangeNotifierProvider).enableExchange) Container( decoration: BoxDecoration( color: Theme.of(context) diff --git a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart index 65ec6a7b8..9741603ac 100644 --- a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart +++ b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart @@ -44,8 +44,6 @@ class _HomeViewButtonBarState extends ConsumerState<HomeViewButtonBar> { @override Widget build(BuildContext context) { - //todo: check if print needed - // debugPrint("BUILD: HomeViewButtonBar"); final selectedIndex = ref.watch(homeViewPageIndexStateProvider.state).state; return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/pages/intro_view.dart b/lib/pages/intro_view.dart index 27e694ff4..7a97eb4a0 100644 --- a/lib/pages/intro_view.dart +++ b/lib/pages/intro_view.dart @@ -49,6 +49,8 @@ class _IntroViewState extends ConsumerState<IntroView> { @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType "); + final stack = + ref.watch(themeProvider.select((value) => value.assets.stack)); return Background( child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, @@ -68,16 +70,22 @@ class _IntroViewState extends ConsumerState<IntroView> { constraints: const BoxConstraints( maxWidth: 300, ), - child: SvgPicture.file( - File( - ref.watch( - themeProvider.select( - (value) => value.assets.stack, - ), - ), - ), + child: SizedBox( width: isDesktop ? 324 : 266, height: isDesktop ? 324 : 266, + child: (stack.endsWith(".png")) + ? Image.file( + File( + stack, + ), + ) + : SvgPicture.file( + File( + stack, + ), + width: isDesktop ? 324 : 266, + height: isDesktop ? 324 : 266, + ), ), ), ), @@ -163,7 +171,7 @@ class _IntroViewState extends ConsumerState<IntroView> { ), if (isDesktop) SecondaryButton( - label: "Restore from Stack backup", + label: "Restore from ${AppConfig.prefix} backup", onPressed: () { Navigator.of(context).pushNamed( CreatePasswordView.routeName, @@ -306,7 +314,7 @@ class GetStartedButton extends StatelessWidget { ); }, child: Text( - "Create new Stack", + "Create new ${AppConfig.prefix}", style: STextStyles.button(context).copyWith(fontSize: 20), ), ), diff --git a/lib/pages/pinpad_views/lock_screen_view.dart b/lib/pages/pinpad_views/lock_screen_view.dart index a2b61c404..3c52c8efc 100644 --- a/lib/pages/pinpad_views/lock_screen_view.dart +++ b/lib/pages/pinpad_views/lock_screen_view.dart @@ -12,6 +12,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; @@ -70,6 +71,7 @@ class LockscreenView extends ConsumerStatefulWidget { class _LockscreenViewState extends ConsumerState<LockscreenView> { late final ShakeController _shakeController; + late final bool _autoPin; late int _attempts; bool _attemptLock = false; @@ -186,16 +188,18 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { biometrics = widget.biometrics; _attempts = 0; _timeout = Duration.zero; - + _autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + if (_autoPin) { + _pinTextController.addListener(_onPinChangedAutologinCheck); + } _checkUseBiometrics(); - _pinTextController.addListener(_onPinChanged); super.initState(); } @override dispose() { // _shakeController.dispose(); - _pinTextController.removeListener(_onPinChanged); + _pinTextController.removeListener(_onPinChangedAutologinCheck); super.dispose(); } @@ -218,16 +222,120 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { final _pinTextController = TextEditingController(); - void _onPinChanged() async { - String enteredPin = _pinTextController.text; - final storedPin = await _secureStore.read(key: 'stack_pin'); - final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + final Mutex _autoPinCheckLock = Mutex(); + void _onPinChangedAutologinCheck() async { + if (mounted) { + await _autoPinCheckLock.acquire(); + } - if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { + try { + if (_autoPin && _pinTextController.text.length >= 4) { + final storedPin = await _secureStore.read(key: 'stack_pin'); + if (_pinTextController.text == storedPin) { + await Future<void>.delayed( + const Duration(milliseconds: 200), + ); + unawaited(_onUnlock()); + } + } + } finally { + _autoPinCheckLock.release(); + } + } + + void _onSubmitPin(String pin) async { + _attempts++; + + if (_attempts > maxAttemptsBeforeThrottling) { + _attemptLock = true; + switch (_attempts) { + case 4: + _timeout = const Duration(seconds: 30); + break; + + case 5: + _timeout = const Duration(seconds: 60); + break; + + case 6: + _timeout = const Duration(minutes: 5); + break; + + case 7: + _timeout = const Duration(minutes: 10); + break; + + case 8: + _timeout = const Duration(minutes: 20); + break; + + case 9: + _timeout = const Duration(minutes: 30); + break; + + default: + _timeout = const Duration(minutes: 60); + } + + _timer?.cancel(); + _timer = Timer(_timeout, () { + _attemptLock = false; + _attempts = 0; + }); + } + + if (_attemptLock) { + String prettyTime = ""; + if (_timeout.inSeconds >= 60) { + prettyTime += "${_timeout.inMinutes} minutes"; + } else { + prettyTime += "${_timeout.inSeconds} seconds"; + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: + "Incorrect PIN entered too many times. Please wait $prettyTime", + context: context, + iconAsset: Assets.svg.alertCircle, + ), + ); + + await Future<void>.delayed( + const Duration(milliseconds: 100), + ); + + _pinTextController.text = ''; + + return; + } + + final storedPin = await _secureStore.read(key: 'stack_pin'); + + if (storedPin == pin) { await Future<void>.delayed( const Duration(milliseconds: 200), ); unawaited(_onUnlock()); + } else { + unawaited(_shakeController.shake()); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: "Incorrect PIN. Please try again", + context: context, + iconAsset: Assets.svg.alertCircle, + ), + ); + } + + await Future<void>.delayed( + const Duration(milliseconds: 100), + ); + + _pinTextController.text = ''; } } @@ -329,98 +437,9 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> { isRandom: ref .read(prefsChangeNotifierProvider) .randomizePIN, - onSubmit: (String pin) async { - _attempts++; - - if (_attempts > maxAttemptsBeforeThrottling) { - _attemptLock = true; - switch (_attempts) { - case 4: - _timeout = const Duration(seconds: 30); - break; - - case 5: - _timeout = const Duration(seconds: 60); - break; - - case 6: - _timeout = const Duration(minutes: 5); - break; - - case 7: - _timeout = const Duration(minutes: 10); - break; - - case 8: - _timeout = const Duration(minutes: 20); - break; - - case 9: - _timeout = const Duration(minutes: 30); - break; - - default: - _timeout = const Duration(minutes: 60); - } - - _timer?.cancel(); - _timer = Timer(_timeout, () { - _attemptLock = false; - _attempts = 0; - }); - } - - if (_attemptLock) { - String prettyTime = ""; - if (_timeout.inSeconds >= 60) { - prettyTime += "${_timeout.inMinutes} minutes"; - } else { - prettyTime += "${_timeout.inSeconds} seconds"; - } - - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: - "Incorrect PIN entered too many times. Please wait $prettyTime", - context: context, - iconAsset: Assets.svg.alertCircle, - ), - ); - - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); - - _pinTextController.text = ''; - - return; - } - - final storedPin = - await _secureStore.read(key: 'stack_pin'); - - if (storedPin == pin) { - await Future<void>.delayed( - const Duration(milliseconds: 200), - ); - unawaited(_onUnlock()); - } else { - unawaited(_shakeController.shake()); - unawaited( - showFloatingFlushBar( - type: FlushBarType.warning, - message: "Incorrect PIN. Please try again", - context: context, - iconAsset: Assets.svg.alertCircle, - ), - ); - - await Future<void>.delayed( - const Duration(milliseconds: 100), - ); - - _pinTextController.text = ''; + onSubmit: (pin) { + if (!_autoPinCheckLock.isLocked) { + _onSubmitPin(pin); } }, ), diff --git a/lib/pages/pinpad_views/pinpad_dialog.dart b/lib/pages/pinpad_views/pinpad_dialog.dart index 35d60aa32..68dbc144c 100644 --- a/lib/pages/pinpad_views/pinpad_dialog.dart +++ b/lib/pages/pinpad_views/pinpad_dialog.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mutex/mutex.dart'; import '../../notifications/show_flush_bar.dart'; import '../../providers/global/prefs_provider.dart'; @@ -38,6 +39,8 @@ class PinpadDialog extends ConsumerStatefulWidget { class _PinpadDialogState extends ConsumerState<PinpadDialog> { late final ShakeController _shakeController; + late final bool _autoPin; + late int _attempts; bool _attemptLock = false; late Duration _timeout; @@ -63,16 +66,24 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> { ); } - Future<void> _onPinChanged() async { - final enteredPin = _pinTextController.text; - final storedPin = await _secureStore.read(key: 'stack_pin'); - final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + final Mutex _autoPinCheckLock = Mutex(); + void _onPinChangedAutologinCheck() async { + if (mounted) { + await _autoPinCheckLock.acquire(); + } - if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { - await Future<void>.delayed( - const Duration(milliseconds: 200), - ); - unawaited(_onUnlock()); + try { + if (_autoPin && _pinTextController.text.length >= 4) { + final storedPin = await _secureStore.read(key: 'stack_pin'); + if (_pinTextController.text == storedPin) { + await Future<void>.delayed( + const Duration(milliseconds: 200), + ); + unawaited(_onUnlock()); + } + } + } finally { + _autoPinCheckLock.release(); } } @@ -215,16 +226,19 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> { biometrics = widget.biometrics; _attempts = 0; _timeout = Duration.zero; + _autoPin = ref.read(prefsChangeNotifierProvider).autoPin; + if (_autoPin) { + _pinTextController.addListener(_onPinChangedAutologinCheck); + } _checkUseBiometrics(); - _pinTextController.addListener(_onPinChanged); super.initState(); } @override dispose() { // _shakeController.dispose(); - _pinTextController.removeListener(_onPinChanged); + _pinTextController.removeListener(_onPinChangedAutologinCheck); super.dispose(); } @@ -276,7 +290,11 @@ class _PinpadDialogState extends ConsumerState<PinpadDialog> { submittedFieldDecoration: _pinPutDecoration, isRandom: ref.read(prefsChangeNotifierProvider).randomizePIN, - onSubmit: _onSubmit, + onSubmit: (pin) { + if (!_autoPinCheckLock.isLocked) { + _onSubmit(pin); + } + }, ), const SizedBox( height: 32, diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index eb102bfe7..2b0e09187 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -22,6 +22,7 @@ import '../../../utilities/address_utils.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/intermediate/bip39_hd_wallet.dart'; import '../../../widgets/address_private_key.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; @@ -371,13 +372,17 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> { detail: address.subType.prettyName, button: Container(), ), - const _Div( - height: 12, - ), - AddressPrivateKey( - walletId: widget.walletId, - address: address, - ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is Bip39HDWallet) + const _Div( + height: 12, + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is Bip39HDWallet) + AddressPrivateKey( + walletId: widget.walletId, + address: address, + ), if (!isDesktop) const SizedBox( height: 20, diff --git a/lib/pages/send_view/frost_ms/frost_send_view.dart b/lib/pages/send_view/frost_ms/frost_send_view.dart index a150ac726..3d90a896f 100644 --- a/lib/pages/send_view/frost_ms/frost_send_view.dart +++ b/lib/pages/send_view/frost_ms/frost_send_view.dart @@ -92,7 +92,6 @@ class _FrostSendViewState extends ConsumerState<FrostSendView> { final txData = await wallet.frostCreateSignConfig( txData: TxData(recipients: recipients), - changeAddress: (await wallet.getCurrentReceivingAddress())!.value, feePerWeight: customFeeRate, ); diff --git a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart index de0ade7df..c96f15233 100644 --- a/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart +++ b/lib/pages/send_view/frost_ms/send_steps/frost_send_step_3.dart @@ -175,7 +175,7 @@ class _FrostSendStep3State extends ConsumerState<FrostSendStep3> { PrimaryButton( label: "Generate transaction", enabled: _userVerifyContinue && - !fieldIsEmptyFlags.reduce((v, e) => v |= e), + !fieldIsEmptyFlags.fold(false, (v, e) => v |= e), onPressed: () async { // collect Share strings final sharesCollected = controllers.map((e) => e.text).toList(); diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 60c6799a3..0aab33a02 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -942,6 +942,9 @@ class _SendViewState extends ConsumerState<SendView> { if (isPaynymSend) { sendToController.text = widget.accountLite!.nymName; noteController.text = "PayNym send"; + WidgetsBinding.instance.addPostFrameCallback( + (_) => _setValidAddressProviders(sendToController.text), + ); } // if (coin is! Epiccash) { diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index 3d5413798..308b83db7 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tuple/tuple.dart'; +import '../../../../app_config.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; @@ -182,6 +183,55 @@ class AdvancedSettingsView extends StatelessWidget { }, ), ), + // showExchange pref. + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable exchange features", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableExchange, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .enableExchange = newValue; + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), const SizedBox( height: 8, ), @@ -218,7 +268,7 @@ class AdvancedSettingsView extends StatelessWidget { text: TextSpan( children: [ TextSpan( - text: "Stack Experience", + text: "${AppConfig.prefix} Experience", style: STextStyles.titleBold12(context), ), TextSpan( diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart index 518cf32da..81297430e 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/debug_view.dart @@ -24,6 +24,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:lelantus/git_versions.dart' as FIRO_VERSIONS; import 'package:package_info_plus/package_info_plus.dart'; +import '../../../../app_config.dart'; import '../../../../models/isar/models/log.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/debug_service_provider.dart'; @@ -421,7 +422,7 @@ class _DebugViewState extends ConsumerState<DebugView> { }, child: CustomLoadingOverlay( message: - "Generating Stack logs file", + "Generating ${AppConfig.prefix} logs file", eventBus: eventBus, ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart index a50568e52..1260aa70d 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app_config.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/block_explorers.dart'; import '../../../../utilities/text_styles.dart'; @@ -91,7 +92,7 @@ class _ManageExplorerViewState extends ConsumerState<ManageExplorerView> { "every block explorer has a slightly different URL " "scheme.\n\nPaste in your block explorer of choice," " then edit in [TXID] where the transaction ID " - "should go, and Stack Wallet will auto fill the " + "should go, and ${AppConfig.appName} will auto fill the " "transaction ID in that place of URL.", style: STextStyles.itemSubtitle(context), ), diff --git a/lib/pages/settings_views/global_settings_view/global_settings_view.dart b/lib/pages/settings_views/global_settings_view/global_settings_view.dart index 38b85edbe..082d03429 100644 --- a/lib/pages/settings_views/global_settings_view/global_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/global_settings_view.dart @@ -99,7 +99,7 @@ class GlobalSettingsView extends StatelessWidget { SettingsListButton( iconAssetName: Assets.svg.downloadFolder, iconSize: 14, - title: "Stack backup & restore", + title: "${AppConfig.prefix} backup & restore", onPressed: () { Navigator.push( context, @@ -113,9 +113,9 @@ class GlobalSettingsView extends StatelessWidget { biometricsCancelButtonString: "CANCEL", biometricsLocalizedReason: - "Authenticate to access Stack backup & restore settings", + "Authenticate to access ${AppConfig.prefix} backup & restore settings", biometricsAuthenticationTitle: - "Stack backup", + "${AppConfig.prefix} backup", ), settings: const RouteSettings( name: "/swblockscreen", diff --git a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart index 4c4bfa66c..57fa710ad 100644 --- a/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart +++ b/lib/pages/settings_views/global_settings_view/security_views/change_pin_view/change_pin_view.dart @@ -61,12 +61,9 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { int pinCount = 1; - final TextEditingController _pinTextController = TextEditingController(); - @override void initState() { _secureStore = ref.read(secureStoreProvider); - _pinTextController.addListener(_onPinChanged); super.initState(); } @@ -77,23 +74,9 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> { _pinPutController2.dispose(); _pinPutFocusNode1.dispose(); _pinPutFocusNode2.dispose(); - _pinTextController.removeListener(_onPinChanged); super.dispose(); } - void _onPinChanged() async { - String enteredPin = _pinTextController.text; - final storedPin = await _secureStore.read(key: 'stack_pin'); - final autoPin = ref.read(prefsChangeNotifierProvider).autoPin; - - if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) { - await _pageController.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - } - @override Widget build(BuildContext context) { return Background( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart index fdfda272c..f41891bb6 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/auto_backup_view.dart @@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../../../../app_config.dart'; import '../../../../providers/global/auto_swb_service_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; @@ -316,7 +317,7 @@ class _AutoBackupViewState extends ConsumerState<AutoBackupView> { children: [ const TextSpan( text: - "Auto Backup is a custom Stack Wallet feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ", + "Auto Backup is a custom ${AppConfig.appName} feature that offers a convenient backup of your data.\n\nTo ensure maximum security, we recommend using a unique password that you haven't used anywhere else on the internet before. Your password is not stored.\n\nFor more information, please see our website ", ), TextSpan( text: "stackwallet.com.", diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart index 1583950c5..4e1fdea36 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart @@ -18,6 +18,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; @@ -651,12 +652,12 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> { builder: (_) => Platform.isAndroid ? StackOkDialog( title: - "Stack Auto Backup enabled and saved to:", + "${AppConfig.prefix} Auto Backup enabled and saved to:", message: fileToSave, ) : const StackOkDialog( title: - "Stack Auto Backup enabled!", + "${AppConfig.prefix} Auto Backup enabled!", ), ); if (mounted) { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart index e762d9f5b..4bf75c0bf 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart @@ -17,6 +17,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/secure_store_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -774,7 +775,7 @@ class _RestoreFromFileViewState extends State<CreateBackupView> { height: 26, ), Text( - "Stack backup saved to: \n", + "${AppConfig.prefix} backup saved to: \n", style: STextStyles .desktopH3(context), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart index f48c0017c..7d944e278 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/dialogs/cancel_stack_restore_dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; +import '../../../../../app_config.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/text_styles.dart'; import '../../../../../utilities/util.dart'; @@ -89,7 +90,7 @@ class CancelStackRestoreDialog extends StatelessWidget { .snackBarBackError, child: Text( "If you cancel, the restore will not complete, and " - "the wallets will not appear in your Stack.", + "the wallets will not appear in your ${AppConfig.prefix}.", style: STextStyles.desktopTextMedium(context), ), ), diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart index 5d495b3d9..ae88abd24 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart @@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/secure_store_provider.dart'; @@ -221,10 +222,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> { barrierDismissible: false, builder: (_) => Platform.isAndroid ? StackOkDialog( - title: "Stack Auto Backup saved to:", + title: "${AppConfig.prefix} Auto Backup saved to:", message: fileToSave, ) - : const StackOkDialog(title: "Stack Auto Backup saved"), + : const StackOkDialog( + title: "${AppConfig.prefix} Auto Backup saved"), ); if (mounted) { passwordController.text = ""; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart index 2b480d59b..001279117 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart @@ -14,6 +14,8 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; + +import '../../../../../app_config.dart'; import '../../../../../utilities/util.dart'; class SWBFileSystem { @@ -39,17 +41,18 @@ class SWBFileSystem { // debugPrint(rootPath!.absolute.toString()); late Directory sampleFolder; + const dirName = "${AppConfig.prefix}_backup"; if (Platform.isIOS) { sampleFolder = Directory(rootPath!.path); } else if (Platform.isAndroid) { - sampleFolder = Directory('${rootPath!.path}Documents/Stack_backups'); + sampleFolder = Directory('${rootPath!.path}Documents/$dirName'); } else if (Platform.isLinux) { - sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + sampleFolder = Directory('${rootPath!.path}/$dirName'); } else if (Platform.isWindows) { - sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + sampleFolder = Directory('${rootPath!.path}/$dirName'); } else if (Platform.isMacOS) { - sampleFolder = Directory('${rootPath!.path}/Stack_backups'); + sampleFolder = Directory('${rootPath!.path}/$dirName'); } try { @@ -79,17 +82,20 @@ class SWBFileSystem { } Future<void> pickDir(BuildContext context) async { - final String? path; + final String? chosenPath; if (Platform.isIOS) { - path = startPath?.path; + chosenPath = startPath?.path; } else { - path = await FilePicker.platform.getDirectoryPath( + final String path = Platform.isWindows + ? startPath!.path.replaceAll("/", "\\") + : startPath!.path; + chosenPath = await FilePicker.platform.getDirectoryPath( dialogTitle: "Choose Backup location", - initialDirectory: startPath!.path, + initialDirectory: path, lockParentWindow: true, ); } - dirPath = path; + dirPath = chosenPath; } Future<void> openFile(BuildContext context) async { diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart index 881319202..9eff26a7e 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart @@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:tuple/tuple.dart'; +import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; @@ -205,7 +206,7 @@ class _RestoreFromEncryptedStringViewState color: Colors.transparent, child: Center( child: Text( - "Decrypting Stack backup file", + "Decrypting ${AppConfig.prefix} backup file", style: STextStyles.pageTitleH2( context, diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart index 0f9cc5b39..c994d6c7c 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart @@ -338,7 +338,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { color: Colors.transparent, child: Center( child: Text( - "Decrypting Stack backup file", + "Decrypting ${AppConfig.prefix} backup file", style: STextStyles.pageTitleH2( context, ).copyWith( @@ -452,7 +452,7 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> { color: Colors.transparent, child: Center( child: Text( - "Decrypting Stack backup file", + "Decrypting ${AppConfig.prefix} backup file", style: STextStyles.pageTitleH2( context, ).copyWith( diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart index ac90e486a..da5c0f411 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/stack_backup_view.dart @@ -10,9 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'auto_backup_view.dart'; -import 'create_backup_view.dart'; -import 'restore_from_file_view.dart'; + +import '../../../../app_config.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; @@ -20,6 +19,9 @@ import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/rounded_white_container.dart'; +import 'auto_backup_view.dart'; +import 'create_backup_view.dart'; +import 'restore_from_file_view.dart'; class StackBackupView extends StatelessWidget { const StackBackupView({ @@ -42,7 +44,7 @@ class StackBackupView extends StatelessWidget { }, ), title: Text( - "Stack backup", + "${AppConfig.prefix} backup", style: STextStyles.navBarTitle(context), ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart new file mode 100644 index 000000000..14cae2ee0 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart @@ -0,0 +1,199 @@ +import 'dart:async'; + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../../../models/keys/cw_key_data.dart'; +import '../../../../notifications/show_flush_bar.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/clipboard_interface.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/detail_item.dart'; +import '../../../../widgets/qr.dart'; +import '../../../../widgets/rounded_white_container.dart'; + +class CNWalletKeys extends StatefulWidget { + const CNWalletKeys({ + super.key, + required this.cwKeyData, + required this.walletId, + this.clipboardInterface = const ClipboardWrapper(), + }); + + final CWKeyData cwKeyData; + final String walletId; + final ClipboardInterface clipboardInterface; + + @override + State<CNWalletKeys> createState() => _CNWalletKeysState(); +} + +class _CNWalletKeysState extends State<CNWalletKeys> { + late String _currentDropDownValue; + + String _current(String key) => + widget.cwKeyData.keys.firstWhere((e) => e.label == key).key; + + Future<void> _copy() async { + await widget.clipboardInterface.setData( + ClipboardData(text: _current(_currentDropDownValue)), + ); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + } + + @override + void initState() { + _currentDropDownValue = widget.cwKeyData.keys.first.label; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: Util.isDesktop + ? const EdgeInsets.symmetric(horizontal: 20) + : EdgeInsets.zero, + child: Column( + mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + DetailItemBase( + horizontal: true, + borderColor: Util.isDesktop + ? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG + : null, + title: Text( + "Selected key", + style: STextStyles.itemSubtitle(context), + ), + detail: SizedBox( + width: Util.isDesktop ? 200 : 170, + child: DropdownButtonHideUnderline( + child: DropdownButton2<String>( + value: _currentDropDownValue, + items: [ + ...widget.cwKeyData.keys.map( + (e) => DropdownMenuItem( + value: e.label, + child: Text( + e.label, + style: STextStyles.w500_14(context), + ), + ), + ), + ], + onChanged: (value) { + if (value is String) { + setState(() { + _currentDropDownValue = value; + }); + } + }, + isExpanded: true, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + ), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + QR( + data: _current(_currentDropDownValue), + size: + Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5, + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + RoundedWhiteContainer( + borderColor: Util.isDesktop + ? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG + : null, + child: SelectableText( + _current(_currentDropDownValue), + style: STextStyles.w500_14(context), + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + if (!Util.isDesktop) const Spacer(), + Row( + children: [ + if (Util.isDesktop) const Spacer(), + if (Util.isDesktop) + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: _copy, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart index 9c1488fcc..95538ed10 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart @@ -13,9 +13,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import '../../../../app_config.dart'; +import '../../../../models/keys/cw_key_data.dart'; +import '../../../../models/keys/key_data_interface.dart'; +import '../../../../models/keys/xpriv_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/address_utils.dart'; @@ -25,17 +27,19 @@ import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../../widgets/desktop/primary_button.dart'; +import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/detail_item.dart'; import '../../../../widgets/qr.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; import '../../../wallet_view/transaction_views/transaction_details_view.dart'; +import 'cn_wallet_keys.dart'; import 'wallet_xprivs.dart'; class WalletBackupView extends ConsumerWidget { @@ -44,8 +48,7 @@ class WalletBackupView extends ConsumerWidget { required this.walletId, required this.mnemonic, this.frostWalletData, - this.clipboardInterface = const ClipboardWrapper(), - this.xprivData, + this.keyData, }); static const String routeName = "/walletBackup"; @@ -58,8 +61,7 @@ class WalletBackupView extends ConsumerWidget { String keys, ({String config, String keys})? prevGen, })? frostWalletData; - final ClipboardInterface clipboardInterface; - final ({List<XPriv> xprivs, String fingerprint})? xprivData; + final KeyDataInterface? keyData; @override Widget build(BuildContext context, WidgetRef ref) { @@ -81,57 +83,29 @@ class WalletBackupView extends ConsumerWidget { style: STextStyles.navBarTitle(context), ), actions: [ - if (xprivData != null) + if (keyData != null) Padding( padding: const EdgeInsets.all(10), child: CustomTextButton( - text: "xpriv(s)", + text: switch (keyData.runtimeType) { + const (XPrivData) => "xpriv(s)", + const (CWKeyData) => "keys", + _ => throw UnimplementedError( + "Don't forget to add your KeyDataInterface here! ${keyData.runtimeType}", + ), + }, onTap: () { Navigator.pushNamed( context, - MobileXPrivsView.routeName, + MobileKeyDataView.routeName, arguments: ( walletId: walletId, - xprivData: xprivData!, + keyData: keyData!, ), ); }, ), ), - if (!frost && xprivData == null) - Padding( - padding: const EdgeInsets.all(10), - child: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - color: - Theme.of(context).extension<StackColors>()!.background, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.copy, - width: 20, - height: 20, - color: Theme.of(context) - .extension<StackColors>()! - .topNavIconPrimary, - ), - onPressed: () async { - await clipboardInterface - .setData(ClipboardData(text: mnemonic.join(" "))); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ), - ); - } - }, - ), - ), - ), ], ), body: Padding( @@ -152,19 +126,22 @@ class WalletBackupView extends ConsumerWidget { } class _Mnemonic extends ConsumerWidget { - const _Mnemonic({super.key, required this.walletId, required this.mnemonic}); + const _Mnemonic({ + super.key, + required this.walletId, + required this.mnemonic, + this.clipboardInterface = const ClipboardWrapper(), + }); final String walletId; final List<String> mnemonic; + final ClipboardInterface clipboardInterface; @override Widget build(BuildContext context, WidgetRef ref) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox( - height: 4, - ), Text( ref.watch(pWalletName(walletId)), textAlign: TextAlign.center, @@ -193,7 +170,11 @@ class _Mnemonic extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(12), child: Text( - "Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.", + "Please write down your backup key. Keep it safe and never share " + "it with anyone. Your backup key is the only way you can access" + " your funds if you forget your PIN, lose your phone, etc.\n\n" + "${AppConfig.appName} does not keep nor is able to restore your" + " backup key. Only you have access to your wallet.", style: STextStyles.label(context), ), ), @@ -212,10 +193,28 @@ class _Mnemonic extends ConsumerWidget { const SizedBox( height: 12, ), - TextButton( - style: Theme.of(context) - .extension<StackColors>()! - .getPrimaryEnabledButtonStyle(context), + SecondaryButton( + label: "Copy", + onPressed: () async { + await clipboardInterface + .setData(ClipboardData(text: mnemonic.join(" "))); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + }, + ), + const SizedBox( + height: 12, + ), + PrimaryButton( + label: "Show QR Code", onPressed: () { final String data = AddressUtils.encodeQRSeedData(mnemonic); @@ -284,10 +283,6 @@ class _Mnemonic extends ConsumerWidget { }, ); }, - child: Text( - "Show QR Code", - style: STextStyles.button(context), - ), ), ], ); @@ -301,8 +296,6 @@ class _FrostKeys extends StatelessWidget { this.frostWalletData, }); - static const String routeName = "/walletBackup"; - final String walletId; final ({ String myName, @@ -433,19 +426,19 @@ class _FrostKeys extends StatelessWidget { } } -class MobileXPrivsView extends StatelessWidget { - const MobileXPrivsView({ +class MobileKeyDataView extends StatelessWidget { + const MobileKeyDataView({ super.key, required this.walletId, this.clipboardInterface = const ClipboardWrapper(), - required this.xprivData, + required this.keyData, }); static const String routeName = "/mobileXPrivView"; final String walletId; final ClipboardInterface clipboardInterface; - final ({List<XPriv> xprivs, String fingerprint}) xprivData; + final KeyDataInterface keyData; @override Widget build(BuildContext context) { @@ -459,7 +452,13 @@ class MobileXPrivsView extends StatelessWidget { }, ), title: Text( - "Wallet xpriv(s)", + "Wallet ${switch (keyData.runtimeType) { + const (XPrivData) => "xpriv(s)", + const (CWKeyData) => "keys", + _ => throw UnimplementedError( + "Don't forget to add your KeyDataInterface here!", + ), + }}", style: STextStyles.navBarTitle(context), ), ), @@ -475,10 +474,19 @@ class MobileXPrivsView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Expanded( - child: WalletXPrivs( - walletId: walletId, - xprivData: xprivData, - ), + child: switch (keyData.runtimeType) { + const (XPrivData) => WalletXPrivs( + walletId: walletId, + xprivData: keyData as XPrivData, + ), + const (CWKeyData) => CNWalletKeys( + walletId: walletId, + cwKeyData: keyData as CWKeyData, + ), + _ => throw UnimplementedError( + "Don't forget to add your KeyDataInterface here!", + ), + }, ), const SizedBox( height: 16, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart index 133dd95a1..594db33a7 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../../models/keys/xpriv_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; @@ -23,7 +24,6 @@ import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../widgets/desktop/primary_button.dart'; import '../../../../widgets/detail_item.dart'; import '../../../../widgets/qr.dart'; @@ -37,7 +37,7 @@ class WalletXPrivs extends ConsumerStatefulWidget { this.clipboardInterface = const ClipboardWrapper(), }); - final ({List<XPriv> xprivs, String fingerprint}) xprivData; + final XPrivData xprivData; final String walletId; final ClipboardInterface clipboardInterface; diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index a9fdd52f0..6985bb00c 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -18,6 +18,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../db/sqlite/firo_cache.dart'; import '../../../models/epicbox_config_model.dart'; +import '../../../models/keys/key_data_interface.dart'; import '../../../notifications/show_flush_bar.dart'; import '../../../providers/global/wallets_provider.dart'; import '../../../providers/ui/transaction_filter_provider.dart'; @@ -35,6 +36,7 @@ import '../../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../../wallets/crypto_currency/intermediate/nano_currency.dart'; import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../../wallets/wallet/impl/epiccash_wallet.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../widgets/background.dart'; @@ -261,10 +263,6 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> { // TODO: [prio=med] take wallets that don't have a mnemonic into account - ({ - List<XPriv> xprivs, - String fingerprint - })? xprivData; List<String>? mnemonic; ({ String myName, @@ -306,8 +304,11 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> { await wallet.getMnemonicAsWords(); } + KeyDataInterface? keyData; if (wallet is ExtendedKeysInterface) { - xprivData = await wallet.getXPrivs(); + keyData = await wallet.getXPrivs(); + } else if (wallet is CwBasedInterface) { + keyData = await wallet.getKeys(); } if (context.mounted) { @@ -323,7 +324,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> { mnemonic: mnemonic ?? [], frostWalletData: frostWalletData, - xprivData: xprivData, + keyData: keyData, ), showBackButton: true, routeOnSuccess: diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart index b33c44c9c..afb5137b4 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../app_config.dart'; import '../../../../providers/providers.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; @@ -69,7 +70,11 @@ class DeleteWalletWarningView extends ConsumerWidget { .extension<StackColors>()! .warningBackground, child: Text( - "You are going to permanently delete your wallet.\n\nIf you delete your wallet, the only way you can have access to your funds is by using your backup key.\n\nStack Wallet does not keep nor is able to restore your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", + "You are going to permanently delete your wallet.\n\n" + "If you delete your wallet, the only way you can have access" + " to your funds is by using your backup key.\n\n" + "${AppConfig.appName} does not keep nor is able to restore " + "your backup key or your wallet.\n\nPLEASE SAVE YOUR BACKUP KEY.", style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension<StackColors>()! diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart index 37040ab71..c02319dcc 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart @@ -11,17 +11,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../providers/db/main_db_provider.dart'; import '../../../../providers/providers.dart'; import '../../../../route_generator.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../../../../wallets/isar/models/wallet_info.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/lelantus_interface.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/rbf_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../../../widgets/background.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../widgets/rounded_white_container.dart'; import '../../../../widgets/stack_dialog.dart'; import '../../../pinpad_views/lock_screen_view.dart'; @@ -31,7 +36,7 @@ import 'rbf_settings_view.dart'; import 'rename_wallet_view.dart'; import 'spark_info.dart'; -class WalletSettingsWalletSettingsView extends ConsumerWidget { +class WalletSettingsWalletSettingsView extends ConsumerStatefulWidget { const WalletSettingsWalletSettingsView({ super.key, required this.walletId, @@ -42,7 +47,88 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { final String walletId; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<WalletSettingsWalletSettingsView> createState() => + _WalletSettingsWalletSettingsViewState(); +} + +class _WalletSettingsWalletSettingsViewState + extends ConsumerState<WalletSettingsWalletSettingsView> { + bool _switchReuseAddressToggledLock = false; // Mutex. + Future<void> _switchReuseAddressToggled(bool newValue) async { + if (newValue) { + await showDialog( + context: context, + builder: (context) { + final isDesktop = Util.isDesktop; + return StackDialog( + title: "Warning!", + message: + "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + leftButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonStyle(context), + child: Text( + "Continue", + style: STextStyles.button(context), + ), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ); + }, + ).then((confirmed) async { + if (_switchReuseAddressToggledLock) { + return; + } + _switchReuseAddressToggledLock = true; // Lock mutex. + + try { + if (confirmed == true) { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: true, + }, + isar: ref.read(mainDBProvider).isar, + ); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } finally { + // ensure _switchReuseAddressToggledLock is set to false no matter what. + _switchReuseAddressToggledLock = false; + } + }); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } + + @override + Widget build(BuildContext context) { return Background( child: Scaffold( backgroundColor: Theme.of(context).extension<StackColors>()!.background, @@ -80,7 +166,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { onPressed: () { Navigator.of(context).pushNamed( RenameWalletView.routeName, - arguments: walletId, + arguments: widget.walletId, ); }, child: Padding( @@ -99,6 +185,172 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { ), ), ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is RbfInterface) + const SizedBox( + height: 8, + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is RbfInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + RbfSettingsView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "RBF settings", + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is MultiAddressInterface) + const SizedBox( + height: 8, + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is MultiAddressInterface) + RoundedWhiteContainer( + child: Consumer( + builder: (_, ref, __) { + return RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Reuse receiving address", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + pWalletInfo(widget.walletId).select( + (value) => value.otherData), + )[WalletInfoKeys.reuseAddress] + as bool? ?? + false, + onValueChanged: (newValue) { + _switchReuseAddressToggled(newValue); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is LelantusInterface) + const SizedBox( + height: 8, + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is LelantusInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + LelantusSettingsView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Lelantus settings", + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is SparkInterface) + const SizedBox( + height: 8, + ), + if (ref.watch(pWallets).getWallet(widget.walletId) + is SparkInterface) + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + SparkInfoView.routeName, + arguments: widget.walletId, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Spark info", + style: STextStyles.titleBold12(context), + ), + ], + ), + ), + ), + ), const SizedBox( height: 8, ), @@ -119,7 +371,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { context: context, builder: (_) => StackDialog( title: - "Do you want to delete ${ref.read(pWalletName(walletId))}?", + "Do you want to delete ${ref.read(pWalletName(widget.walletId))}?", leftButton: TextButton( style: Theme.of(context) .extension<StackColors>()! @@ -148,7 +400,7 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => LockscreenView( - routeOnSuccessArguments: walletId, + routeOnSuccessArguments: widget.walletId, showBackButton: true, routeOnSuccess: DeleteWalletWarningView.routeName, @@ -188,116 +440,6 @@ class WalletSettingsWalletSettingsView extends ConsumerWidget { ), ), ), - if (ref.watch(pWallets).getWallet(walletId) - is LelantusInterface) - const SizedBox( - height: 8, - ), - if (ref.watch(pWallets).getWallet(walletId) - is LelantusInterface) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - LelantusSettingsView.routeName, - arguments: walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Lelantus settings", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - if (ref.watch(pWallets).getWallet(walletId) is SparkInterface) - const SizedBox( - height: 8, - ), - if (ref.watch(pWallets).getWallet(walletId) is SparkInterface) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - SparkInfoView.routeName, - arguments: walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "Spark info", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), - if (ref.watch(pWallets).getWallet(walletId) is RbfInterface) - const SizedBox( - height: 8, - ), - if (ref.watch(pWallets).getWallet(walletId) is RbfInterface) - RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - onPressed: () { - Navigator.of(context).pushNamed( - RbfSettingsView.routeName, - arguments: walletId, - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 20, - ), - child: Row( - children: [ - Text( - "RBF settings", - style: STextStyles.titleBold12(context), - ), - ], - ), - ), - ), - ), ], ), ), diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart index a256b5aea..09200fb1b 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart @@ -10,14 +10,17 @@ import 'dart:async'; +import 'package:dropdown_button2/dropdown_button2.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 '../../../../notifications/show_flush_bar.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; +import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../utilities/util.dart'; import '../../../../wallets/isar/providers/wallet_info_provider.dart'; @@ -25,16 +28,14 @@ import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interfa import '../../../../widgets/background.dart'; import '../../../../widgets/conditional_parent.dart'; import '../../../../widgets/custom_buttons/app_bar_icon_button.dart'; -import '../../../../widgets/custom_tab_view.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; import '../../../../widgets/desktop/primary_button.dart'; -import '../../../../widgets/desktop/secondary_button.dart'; import '../../../../widgets/detail_item.dart'; import '../../../../widgets/qr.dart'; import '../../../../widgets/rounded_white_container.dart'; -class XPubView extends ConsumerWidget { +class XPubView extends ConsumerStatefulWidget { const XPubView({ super.key, required this.walletId, @@ -49,7 +50,39 @@ class XPubView extends ConsumerWidget { static const String routeName = "/xpub"; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<XPubView> createState() => XPubViewState(); +} + +class XPubViewState extends ConsumerState<XPubView> { + late String _currentDropDownValue; + + String _current(String key) => + widget.xpubData.xpubs.firstWhere((e) => e.path == key).xpub; + + Future<void> _copy() async { + await widget.clipboardInterface.setData( + ClipboardData(text: _current(_currentDropDownValue)), + ); + if (mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + ), + ); + } + } + + @override + void initState() { + _currentDropDownValue = widget.xpubData.xpubs.first.path; + super.initState(); + } + + @override + Widget build(BuildContext context) { final bool isDesktop = Util.isDesktop; return ConditionalParent( @@ -75,7 +108,25 @@ class XPubView extends ConsumerWidget { left: 16, right: 16, ), - child: SingleChildScrollView(child: child), + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + Expanded( + child: child, + ), + const SizedBox( + height: 16, + ), + ], + ), + ), + ), + ), + ), ), ), ), @@ -95,7 +146,7 @@ class XPubView extends ConsumerWidget { left: 32, ), child: Text( - "${ref.watch(pWalletName(walletId))} xpub(s)", + "${ref.watch(pWalletName(widget.walletId))} xpub(s)", style: STextStyles.desktopH2(context), ), ), @@ -119,28 +170,146 @@ class XPubView extends ConsumerWidget { ), ), child: Column( - mainAxisSize: MainAxisSize.min, + mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (isDesktop) const SizedBox(height: 16), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), DetailItem( title: "Master fingerprint", - detail: xpubData.fingerprint, + detail: widget.xpubData.fingerprint, horizontal: true, + borderColor: Util.isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : null, ), - if (isDesktop) const SizedBox(height: 16), - CustomTabView( - titles: xpubData.xpubs.map((e) => e.path).toList(), - children: xpubData.xpubs - .map( - (e) => Padding( - padding: const EdgeInsets.only(top: 16), - child: _XPub( - xpub: e.xpub, - derivation: e.path, + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + DetailItemBase( + horizontal: true, + borderColor: Util.isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : null, + title: Text( + "Derivation", + style: STextStyles.itemSubtitle(context), + ), + detail: SizedBox( + width: Util.isDesktop ? 200 : 170, + child: DropdownButtonHideUnderline( + child: DropdownButton2<String>( + value: _currentDropDownValue, + items: [ + ...widget.xpubData.xpubs.map( + (e) => DropdownMenuItem( + value: e.path, + child: Text( + e.path, + style: STextStyles.w500_14(context), + ), + ), + ), + ], + onChanged: (value) { + if (value is String) { + setState(() { + _currentDropDownValue = value; + }); + } + }, + isExpanded: true, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - ) - .toList(), + iconStyleData: IconStyleData( + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 12, + height: 6, + color: Theme.of(context) + .extension<StackColors>()! + .textFieldActiveSearchIconRight, + ), + ), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + QR( + data: _current(_currentDropDownValue), + size: Util.isDesktop + ? 256 + : MediaQuery.of(context).size.width / 1.5, + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + RoundedWhiteContainer( + borderColor: Util.isDesktop + ? Theme.of(context) + .extension<StackColors>()! + .textFieldDefaultBG + : null, + child: SelectableText( + _current(_currentDropDownValue), + style: STextStyles.w500_14(context), + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 16, + ), + if (!Util.isDesktop) const Spacer(), + Row( + children: [ + if (Util.isDesktop) const Spacer(), + if (Util.isDesktop) + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Copy", + onPressed: _copy, + ), + ), + ], ), ], ), @@ -148,91 +317,3 @@ class XPubView extends ConsumerWidget { ); } } - -class _XPub extends StatelessWidget { - const _XPub({ - super.key, - required this.xpub, - required this.derivation, - this.clipboardInterface = const ClipboardWrapper(), - }); - - final String xpub; - final String derivation; - - final ClipboardInterface clipboardInterface; - - @override - Widget build(BuildContext context) { - final bool isDesktop = Util.isDesktop; - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox( - height: 25, - ), - ConditionalParent( - condition: !isDesktop, - builder: (child) => RoundedWhiteContainer( - child: child, - ), - child: QR( - data: xpub, - size: isDesktop ? 280 : MediaQuery.of(context).size.width / 1.5, - ), - ), - const SizedBox(height: 25), - RoundedWhiteContainer( - padding: const EdgeInsets.all(16), - borderColor: - Theme.of(context).extension<StackColors>()!.backgroundAppBar, - child: SelectableText( - xpub, - style: STextStyles.largeMedium14(context), - ), - ), - const SizedBox(height: 32), - Row( - children: [ - if (isDesktop) - Expanded( - child: SecondaryButton( - buttonHeight: ButtonHeight.xl, - label: "Cancel", - onPressed: Navigator.of( - context, - rootNavigator: true, - ).pop, - ), - ), - if (isDesktop) const SizedBox(width: 16), - Expanded( - child: PrimaryButton( - buttonHeight: ButtonHeight.xl, - label: "Copy", - onPressed: () async { - await clipboardInterface.setData( - ClipboardData( - text: xpub, - ), - ); - if (context.mounted) { - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - iconAsset: Assets.svg.copy, - context: context, - ), - ); - } - }, - ), - ), - ], - ), - ], - ); - } -} diff --git a/lib/pages/special/firo_rescan_recovery_error_dialog.dart b/lib/pages/special/firo_rescan_recovery_error_dialog.dart index 6409646c6..92d226d4b 100644 --- a/lib/pages/special/firo_rescan_recovery_error_dialog.dart +++ b/lib/pages/special/firo_rescan_recovery_error_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../models/keys/key_data_interface.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; import '../../providers/global/wallets_provider.dart'; @@ -11,6 +12,7 @@ import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../widgets/background.dart'; @@ -265,9 +267,11 @@ class _FiroRescanRecoveryErrorViewState if (wallet is MnemonicInterface) { final mnemonic = await wallet.getMnemonicAsWords(); - ({List<XPriv> xprivs, String fingerprint})? xprivData; + KeyDataInterface? keyData; if (wallet is ExtendedKeysInterface) { - xprivData = await wallet.getXPrivs(); + keyData = await wallet.getXPrivs(); + } else if (wallet is CwBasedInterface) { + keyData = await wallet.getKeys(); } if (context.mounted) { @@ -280,7 +284,7 @@ class _FiroRescanRecoveryErrorViewState routeOnSuccessArguments: ( walletId: widget.walletId, mnemonic: mnemonic, - xprivData: xprivData, + keyData: keyData, ), showBackButton: true, routeOnSuccess: WalletBackupView.routeName, diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 52b5323c6..5ebaca345 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../app_config.dart'; import '../db/hive/db.dart'; import '../pages_desktop_specific/password/create_password_view.dart'; import '../providers/global/prefs_provider.dart'; @@ -104,190 +105,199 @@ class _StackPrivacyCalls extends ConsumerState<StackPrivacyCalls> { constraints: BoxConstraints( maxWidth: isDesktop ? 480 : double.infinity, ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Choose your Stack experience", - style: isDesktop - ? STextStyles.desktopH2(context) - : STextStyles.pageTitleH1(context), - ), - SizedBox( - height: isDesktop ? 16 : 8, - ), - Text( - !widget.isSettings - ? "You can change it later in Settings" - : "", - style: isDesktop - ? STextStyles.desktopSubtitleH2(context) - : STextStyles.subtitle(context), - ), - SizedBox( - height: isDesktop ? 32 : 36, - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: isDesktop ? 0 : 16, + child: ConditionalParent( + condition: isDesktop, + builder: (child) => SingleChildScrollView( + child: child, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Choose your ${AppConfig.prefix} experience", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), ), - child: PrivacyToggle( - externalCallsEnabled: isEasy, - onChanged: (externalCalls) { - isEasy = externalCalls; - setState(() { - infoToggle = isEasy; - }); - }, + SizedBox( + height: isDesktop ? 16 : 8, ), - ), - SizedBox( - height: isDesktop ? 16 : 36, - ), - Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.all(16.0), - child: RoundedWhiteContainer( - child: Center( - child: RichText( - textAlign: TextAlign.left, - text: TextSpan( - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context, - ) - : STextStyles.label(context).copyWith( - fontSize: 12.0, - ), - children: infoToggle - ? [ - if (Constants.enableExchange) + Text( + !widget.isSettings + ? "You can change it later in Settings" + : "", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 32 : 36, + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 0 : 16, + ), + child: PrivacyToggle( + externalCallsEnabled: isEasy, + onChanged: (externalCalls) { + isEasy = externalCalls; + setState(() { + infoToggle = isEasy; + }); + }, + ), + ), + SizedBox( + height: isDesktop ? 16 : 36, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.all(16.0), + child: RoundedWhiteContainer( + child: Center( + child: RichText( + textAlign: TextAlign.left, + text: TextSpan( + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context, + ) + : STextStyles.label(context).copyWith( + fontSize: 12.0, + ), + children: infoToggle + ? [ + if (Constants.enableExchange) + const TextSpan( + text: + "Exchange data preloaded for a seamless experience.\n\n", + ), const TextSpan( text: - "Exchange data preloaded for a seamless experience.\n\n", + "CoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency).\n\n", ), - const TextSpan( - text: - "CoinGecko enabled: (24 hour price change shown in-app, total wallet value shown in USD or other currency).\n\n", - ), - TextSpan( - text: - "Recommended for most crypto users.", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall600( - context, - ) - : TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, - ), - ), - ] - : [ - if (Constants.enableExchange) + TextSpan( + text: + "Recommended for most crypto users.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context, + ) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ] + : [ + if (Constants.enableExchange) + const TextSpan( + text: + "Exchange data not preloaded (slower experience).\n\n", + ), const TextSpan( text: - "Exchange data not preloaded (slower experience).\n\n", + "CoinGecko disabled (price changes not shown, no wallet value shown in other currencies).\n\n", ), - const TextSpan( - text: - "CoinGecko disabled (price changes not shown, no wallet value shown in other currencies).\n\n", - ), - TextSpan( - text: - "Recommended for the privacy conscious.", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall600( - context, - ) - : TextStyle( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - fontWeight: FontWeight.w600, - ), - ), - ], + TextSpan( + text: + "Recommended for the privacy conscious.", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall600( + context, + ) + : TextStyle( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + fontWeight: FontWeight.w600, + ), + ), + ], + ), ), ), ), ), - ), - if (!isDesktop) - const Spacer( - flex: 4, - ), - if (isDesktop) - const SizedBox( - height: 32, - ), - Padding( - padding: isDesktop - ? const EdgeInsets.all(0) - : const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - children: [ - Expanded( - child: PrimaryButton( - label: !widget.isSettings - ? "Continue" - : "Save changes", - onPressed: () { - ref - .read(prefsChangeNotifierProvider) - .externalCalls = isEasy; + if (!isDesktop) + const Spacer( + flex: 4, + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.all(0) + : const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + children: [ + Expanded( + child: PrimaryButton( + label: !widget.isSettings + ? "Continue" + : "Save changes", + onPressed: () { + ref + .read(prefsChangeNotifierProvider) + .externalCalls = isEasy; - DB.instance - .put<dynamic>( - boxName: DB.boxNamePrefs, - key: "externalCalls", - value: isEasy, - ) - .then((_) { - if (isEasy) { - unawaited( - ExchangeDataLoadingService.instance - .loadAll(), - ); - // unawaited( - // BuyDataLoadingService().loadAll(ref)); - ref - .read(priceAnd24hChangeNotifierProvider) - .start(true); - } - }); - if (!widget.isSettings) { - if (isDesktop) { - Navigator.of(context).pushNamed( - CreatePasswordView.routeName, - ); + DB.instance + .put<dynamic>( + boxName: DB.boxNamePrefs, + key: "externalCalls", + value: isEasy, + ) + .then((_) { + if (isEasy) { + if (AppConfig.hasFeature(AppFeature.swap)) { + unawaited( + ExchangeDataLoadingService.instance + .loadAll(), + ); + } + // unawaited( + // BuyDataLoadingService().loadAll(ref)); + ref + .read(priceAnd24hChangeNotifierProvider) + .start(true); + } + }); + if (!widget.isSettings) { + if (isDesktop) { + Navigator.of(context).pushNamed( + CreatePasswordView.routeName, + ); + } else { + Navigator.of(context).pushNamed( + CreatePinView.routeName, + ); + } } else { - Navigator.of(context).pushNamed( - CreatePinView.routeName, - ); + Navigator.pop(context); } - } else { - Navigator.pop(context); - } - }, + }, + ), ), - ), - ], + ], + ), ), - ), - if (isDesktop) - const SizedBox( - height: kDesktopAppBarHeight, - ), - ], + if (isDesktop) + const SizedBox( + height: kDesktopAppBarHeight, + ), + ], + ), ), ), ), diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart index 442721b54..f5b051ef7 100644 --- a/lib/pages/token_view/sub_widgets/token_summary.dart +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -218,6 +218,9 @@ class TokenWalletOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(prefsChangeNotifierProvider); + final showExchange = prefs.enableExchange; + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -251,11 +254,11 @@ class TokenWalletOptions extends ConsumerWidget { subLabel: "Send", iconAssetPathSVG: Assets.svg.arrowUpRight, ), - if (AppConfig.hasFeature(AppFeature.swap)) + if (AppConfig.hasFeature(AppFeature.swap) && showExchange) const SizedBox( width: 16, ), - if (AppConfig.hasFeature(AppFeature.swap)) + if (AppConfig.hasFeature(AppFeature.swap) && showExchange) TokenOptionsButton( onPressed: () => _onExchangePressed(context), subLabel: "Swap", @@ -265,11 +268,11 @@ class TokenWalletOptions extends ConsumerWidget { ), ), ), - if (AppConfig.hasFeature(AppFeature.buy)) + if (AppConfig.hasFeature(AppFeature.buy) && showExchange) const SizedBox( width: 16, ), - if (AppConfig.hasFeature(AppFeature.buy)) + if (AppConfig.hasFeature(AppFeature.buy) && showExchange) TokenOptionsButton( onPressed: () => _onBuyPressed(context), subLabel: "Buy", diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 232e3ee2c..5afd82597 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -960,8 +960,7 @@ class _DesktopTransactionCardRowState unawaited( showFloatingFlushBar( context: context, - message: - "Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.", + message: "Restored Epic funds from your Seed have no Data.", type: FlushBarType.warning, duration: const Duration(seconds: 5), ), diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index dad531969..1071ffdae 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -15,14 +15,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tuple/tuple.dart'; +import 'package:url_launcher/url_launcher.dart'; + import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/ethereum/eth_contract.dart'; import '../../../notifications/show_flush_bar.dart'; -import '../../receive_view/addresses/address_details_view.dart'; -import '../sub_widgets/tx_icon.dart'; -import 'dialogs/cancelling_transaction_progress_dialog.dart'; -import 'edit_note_view.dart'; -import '../wallet_view.dart'; import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/address_book_service_provider.dart'; import '../../../providers/providers.dart'; @@ -52,8 +50,11 @@ import '../../../widgets/icon_widgets/copy_icon.dart'; import '../../../widgets/icon_widgets/pencil_icon.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_dialog.dart'; -import 'package:tuple/tuple.dart'; -import 'package:url_launcher/url_launcher.dart'; +import '../../receive_view/addresses/address_details_view.dart'; +import '../sub_widgets/tx_icon.dart'; +import '../wallet_view.dart'; +import 'dialogs/cancelling_transaction_progress_dialog.dart'; +import 'edit_note_view.dart'; class TransactionDetailsView extends ConsumerStatefulWidget { const TransactionDetailsView({ @@ -133,13 +134,15 @@ class _TransactionDetailsViewState } String whatIsIt(Transaction tx, int height) { + String prettyConfirms() => "(${tx.getConfirmations(height)}/$minConfirms)"; + final type = tx.type; if (coin is Firo) { if (tx.subType == TransactionSubType.mint) { if (tx.isConfirmed(height, minConfirms)) { return "Minted"; } else { - return "Minting"; + return "Minting ${prettyConfirms()}"; } } } @@ -156,7 +159,7 @@ class _TransactionDetailsViewState } else if ((_transaction.numberOfMessages ?? 0) > 1) { return "Receiving (waiting for confirmations)"; // TODO test if the sender still has to open again after the receiver has 2 messages present, ie. sender->receiver->sender->node (yes) vs. sender->receiver->node (no) } else { - return "Receiving"; + return "Receiving ${prettyConfirms()}"; } } } else if (type == TransactionType.outgoing) { @@ -168,7 +171,7 @@ class _TransactionDetailsViewState } else if ((_transaction.numberOfMessages ?? 0) > 1) { return "Sending (waiting for confirmations)"; } else { - return "Sending"; + return "Sending ${prettyConfirms()}"; } } } @@ -181,16 +184,20 @@ class _TransactionDetailsViewState if (tx.isConfirmed(height, minConfirms)) { return "Received"; } else { - return "Receiving"; + return "Receiving ${prettyConfirms()}"; } } else if (type == TransactionType.outgoing) { if (tx.isConfirmed(height, minConfirms)) { return "Sent"; } else { - return "Sending"; + return "Sending ${prettyConfirms()}"; } } else if (type == TransactionType.sentToSelf) { - return "Sent to self"; + if (tx.isConfirmed(height, minConfirms)) { + return "Sent to self"; + } else { + return "Sent to self ${prettyConfirms()}"; + } } else { return type.name; } @@ -1157,59 +1164,95 @@ class _TransactionDetailsViewState : const SizedBox( height: 12, ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder( - builder: (context) { - final String height; + Builder( + builder: (context) { + final String height; + final String confirmations; + final confirms = _transaction.getConfirmations( + currentHeight, + ); - if (widget.coin is Bitcoincash || - widget.coin is Ecash) { + if (widget.coin is Bitcoincash || + widget.coin is Ecash) { + height = _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; + confirmations = confirms.toString(); + } else if (widget.coin is Epiccash && + _transaction.slateId == null) { + confirmations = "Unknown"; + height = "Unknown"; + } else { + final confirmed = _transaction.isConfirmed( + currentHeight, + minConfirms, + ); + if (widget.coin is! Epiccash && confirmed) { height = - "${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; + "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; } else { - height = widget.coin is! Epiccash && - _transaction.isConfirmed( - currentHeight, - minConfirms, - ) - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.getConfirmations( - currentHeight, - ) > - 0 - ? "${_transaction.height}" - : "Pending"; + height = confirms > 0 + ? "${_transaction.height}" + : "Pending"; } - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Column( + confirmations = confirms.toString(); + } + + return Column( + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ), + ], ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) + if (!isDesktop) SelectableText( height, style: isDesktop @@ -1226,30 +1269,91 @@ class _TransactionDetailsViewState context, ), ), + if (isDesktop) + IconCopyButton(data: height), ], ), - if (!isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox( + height: 2, ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], - ); - }, - ), + if (isDesktop) + SelectableText( + confirmations, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + confirmations, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + ], + ); + }, ), if (coin is Ethereum) isDesktop diff --git a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart index ce5635c1c..666d8d041 100644 --- a/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/tx_v2/transaction_v2_details_view.dart @@ -1053,6 +1053,12 @@ class _TransactionV2DetailsViewState ], ), ), + if (coin is Epiccash) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), if (coin is Epiccash) RoundedWhiteContainer( padding: isDesktop @@ -1457,59 +1463,95 @@ class _TransactionV2DetailsViewState : const SizedBox( height: 12, ), - RoundedWhiteContainer( - padding: isDesktop - ? const EdgeInsets.all(16) - : const EdgeInsets.all(12), - child: Builder( - builder: (context) { - final String height; + Builder( + builder: (context) { + final String height; + final String confirmations; + final confirms = _transaction.getConfirmations( + currentHeight, + ); - if (widget.coin is Bitcoincash || - widget.coin is Ecash) { + if (widget.coin is Bitcoincash || + widget.coin is Ecash) { + height = _transaction.height != null && + _transaction.height! > 0 + ? "${_transaction.height!}" + : "Pending"; + confirmations = confirms.toString(); + } else if (widget.coin is Epiccash && + _transaction.slateId == null) { + confirmations = "Unknown"; + height = "Unknown"; + } else { + final confirmed = _transaction.isConfirmed( + currentHeight, + minConfirms, + ); + if (widget.coin is! Epiccash && confirmed) { height = - "${_transaction.height != null && _transaction.height! > 0 ? _transaction.height! : "Pending"}"; + "${_transaction.height == 0 ? "Unknown" : _transaction.height}"; } else { - height = widget.coin is! Epiccash && - _transaction.isConfirmed( - currentHeight, - minConfirms, - ) - ? "${_transaction.height == 0 ? "Unknown" : _transaction.height}" - : _transaction.getConfirmations( - currentHeight, - ) > - 0 - ? "${_transaction.height}" - : "Pending"; + height = confirms > 0 + ? "${_transaction.height}" + : "Pending"; } - return Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Column( + confirmations = confirms.toString(); + } + + return Column( + children: [ + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Block height", - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ) - : STextStyles.itemSubtitle( - context, - ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Block height", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox( + height: 2, + ), + if (isDesktop) + SelectableText( + height, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ), + ], ), - if (isDesktop) - const SizedBox( - height: 2, - ), - if (isDesktop) + if (!isDesktop) SelectableText( height, style: isDesktop @@ -1526,30 +1568,91 @@ class _TransactionV2DetailsViewState context, ), ), + if (isDesktop) + IconCopyButton(data: height), ], ), - if (!isDesktop) - SelectableText( - height, - style: isDesktop - ? STextStyles - .desktopTextExtraExtraSmall( - context, - ).copyWith( - color: Theme.of(context) - .extension<StackColors>()! - .textDark, - ) - : STextStyles.itemSubtitle12( - context, + ), + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ) + : STextStyles.itemSubtitle( + context, + ), + ), + if (isDesktop) + const SizedBox( + height: 2, ), - ), - if (isDesktop) - IconCopyButton(data: height), - ], - ); - }, - ), + if (isDesktop) + SelectableText( + confirmations, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of( + context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles + .itemSubtitle12( + context, + ), + ), + ], + ), + if (!isDesktop) + SelectableText( + confirmations, + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context, + ), + ), + if (isDesktop) + IconCopyButton(data: height), + ], + ), + ), + ], + ); + }, ), if (kDebugMode) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index ea9ca50e4..e4ad804dc 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -518,6 +518,9 @@ class _WalletViewState extends ConsumerState<WalletView> { final coin = ref.watch(pWalletCoin(walletId)); + final prefs = ref.watch(prefsChangeNotifierProvider); + final showExchange = prefs.enableExchange; + return ConditionalParent( condition: _rescanningOnOpen, builder: (child) { @@ -1053,7 +1056,8 @@ class _WalletViewState extends ConsumerState<WalletView> { ), if (Constants.enableExchange && ref.watch(pWalletCoin(walletId)) is! FrostCurrency && - AppConfig.hasFeature(AppFeature.swap)) + AppConfig.hasFeature(AppFeature.swap) && + showExchange) WalletNavigationBarItemData( label: "Swap", icon: const ExchangeNavIcon(), @@ -1061,7 +1065,8 @@ class _WalletViewState extends ConsumerState<WalletView> { ), if (Constants.enableExchange && ref.watch(pWalletCoin(walletId)) is! FrostCurrency && - AppConfig.hasFeature(AppFeature.buy)) + AppConfig.hasFeature(AppFeature.buy) && + showExchange) WalletNavigationBarItemData( label: "Buy", icon: const BuyNavIcon(), diff --git a/lib/pages/wallets_view/sub_widgets/empty_wallets.dart b/lib/pages/wallets_view/sub_widgets/empty_wallets.dart index 53581c2f8..c9a59005f 100644 --- a/lib/pages/wallets_view/sub_widgets/empty_wallets.dart +++ b/lib/pages/wallets_view/sub_widgets/empty_wallets.dart @@ -33,6 +33,9 @@ class EmptyWallets extends ConsumerWidget { final isDesktop = Util.isDesktop; + final stack = + ref.watch(themeProvider.select((value) => value.assets.stack)); + return SafeArea( child: Padding( padding: const EdgeInsets.symmetric( @@ -47,21 +50,28 @@ class EmptyWallets extends ConsumerWidget { const Spacer( flex: 2, ), - SvgPicture.file( - File( - ref.watch( - themeProvider.select( - (value) => value.assets.stack, - ), - ), - ), + SizedBox( width: isDesktop ? 324 : MediaQuery.of(context).size.width / 3, + child: (stack.endsWith(".png")) + ? Image.file( + File( + stack, + ), + ) + : SvgPicture.file( + File( + stack, + ), + width: isDesktop + ? 324 + : MediaQuery.of(context).size.width / 3, + ), ), SizedBox( height: isDesktop ? 30 : 16, ), Text( - "You do not have any wallets yet. Start building your crypto Stack!", + AppConfig.emptyWalletsMessage, textAlign: TextAlign.center, style: isDesktop ? STextStyles.desktopSubtitleH2(context).copyWith( diff --git a/lib/pages/wallets_view/wallets_overview.dart b/lib/pages/wallets_view/wallets_overview.dart index 6fb6cc718..b5d73dbaf 100644 --- a/lib/pages/wallets_view/wallets_overview.dart +++ b/lib/pages/wallets_view/wallets_overview.dart @@ -12,7 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:tuple/tuple.dart'; import '../../app_config.dart'; import '../../models/add_wallet_list_entity/sub_classes/coin_entity.dart'; @@ -20,6 +19,8 @@ import '../../models/isar/models/ethereum/eth_contract.dart'; import '../../pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart'; import '../../providers/db/main_db_provider.dart'; import '../../providers/providers.dart'; +import '../../services/event_bus/events/wallet_added_event.dart'; +import '../../services/event_bus/global_event_bus.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; @@ -58,6 +59,8 @@ class WalletsOverview extends ConsumerStatefulWidget { ConsumerState<WalletsOverview> createState() => _EthWalletsOverviewState(); } +typedef WalletListItemData = ({Wallet wallet, List<EthContract> contracts}); + class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { final isDesktop = Util.isDesktop; @@ -66,28 +69,29 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { String _searchString = ""; - final List<Tuple2<Wallet, List<EthContract>>> wallets = []; + final Map<String, WalletListItemData> wallets = {}; - List<Tuple2<Wallet, List<EthContract>>> _filter(String searchTerm) { + List<WalletListItemData> _filter(String searchTerm) { if (searchTerm.isEmpty) { - return wallets; + return wallets.values.toList() + ..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name)); } - final List<Tuple2<Wallet, List<EthContract>>> results = []; + final Map<String, WalletListItemData> results = {}; final term = searchTerm.toLowerCase(); - for (final tuple in wallets) { + for (final entry in wallets.entries) { bool includeManager = false; // search wallet name and total balance - includeManager |= _elementContains(tuple.item1.info.name, term); + includeManager |= _elementContains(entry.value.wallet.info.name, term); includeManager |= _elementContains( - tuple.item1.info.cachedBalance.total.decimal.toString(), + entry.value.wallet.info.cachedBalance.total.decimal.toString(), term, ); final List<EthContract> contracts = []; - for (final contract in tuple.item2) { + for (final contract in entry.value.contracts) { if (_elementContains(contract.name, term)) { contracts.add(contract); } else if (_elementContains(contract.symbol, term)) { @@ -100,22 +104,19 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { } if (includeManager || contracts.isNotEmpty) { - results.add(Tuple2(tuple.item1, contracts)); + results.addEntries([entry]); } } - return results; + return results.values.toList() + ..sort((a, b) => a.wallet.info.name.compareTo(b.wallet.info.name)); } bool _elementContains(String element, String term) { return element.toLowerCase().contains(term); } - @override - void initState() { - _searchController = TextEditingController(); - searchFieldFocusNode = FocusNode(); - + void updateWallets() { final walletsData = ref.read(mainDBProvider).isar.walletInfo.where().findAllSync(); walletsData.removeWhere((e) => e.coin != widget.coin); @@ -143,28 +144,48 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { } // add tuple to list - wallets.add( - Tuple2( - ref.read(pWallets).getWallet( - data.walletId, - ), - contracts, - ), + wallets[data.walletId] = ( + wallet: ref.read(pWallets).getWallet( + data.walletId, + ), + contracts: contracts, ); } } else { // add non token wallet tuple to list for (final data in walletsData) { - wallets.add( - Tuple2( - ref.read(pWallets).getWallet( + // desktop single coin apps may cause issues so lets just ignore the error and move on + try { + wallets[data.walletId] = ( + wallet: ref.read(pWallets).getWallet( data.walletId, ), - [], - ), - ); + contracts: [], + ); + } catch (_) { + // lol bandaid for single coin based apps + } } } + } + + @override + void initState() { + _searchController = TextEditingController(); + searchFieldFocusNode = FocusNode(); + + updateWallets(); + + if (AppConfig.isSingleCoinApp) { + GlobalEventBus.instance.on<WalletAddedEvent>().listen((_) { + updateWallets(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() {}); + } + }); + }); + } super.initState(); } @@ -293,24 +314,27 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { final data = _filter(_searchString); return ListView.separated( itemBuilder: (_, index) { - final element = data[index]; + final entry = data[index]; + final wallet = entry.wallet; - if (element.item1.cryptoCurrency.hasTokenSupport) { + if (wallet.cryptoCurrency.hasTokenSupport) { if (isDesktop) { return DesktopExpandingWalletCard( key: Key( - "${element.item1.info.name}_${element.item2.map((e) => e.address).join()}", + "${wallet.walletId}_${entry.contracts.map((e) => e.address).join()}", ), - data: element, + data: entry, navigatorState: widget.navigatorState!, ); } else { return MasterWalletCard( - walletId: element.item1.walletId, + key: Key(wallet.walletId), + walletId: wallet.walletId, ); } } else { return ConditionalParent( + key: Key(wallet.walletId), condition: isDesktop, builder: (child) => RoundedWhiteContainer( padding: const EdgeInsets.symmetric( @@ -323,7 +347,7 @@ class _EthWalletsOverviewState extends ConsumerState<WalletsOverview> { child: child, ), child: SimpleWalletCard( - walletId: element.item1.walletId, + walletId: wallet.walletId, popPrevious: widget .overrideSimpleWalletCardPopPreviousValueWith == null diff --git a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart index 51502bf81..09bce3a3b 100644 --- a/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart +++ b/lib/pages_desktop_specific/cashfusion/sub_widgets/fusion_dialog.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wakelock/wakelock.dart'; import '../../../providers/cash_fusion/fusion_progress_ui_state_provider.dart'; import '../../../providers/global/prefs_provider.dart'; @@ -137,6 +139,8 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> { message: "Stopping fusion", ); + await Wakelock.disable(); + return true; } else { return false; @@ -150,6 +154,12 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> { super.initState(); } + @override + dispose() { + Wakelock.disable(); + super.dispose(); + } + @override Widget build(BuildContext context) { final bool _succeeded = @@ -162,6 +172,10 @@ class _FusionDialogViewState extends ConsumerState<FusionDialogView> { .watch(fusionProgressUIStateProvider(widget.walletId)) .fusionRoundsCompleted; + if (!Platform.isLinux) { + Wakelock.enable(); + } + return DesktopDialog( maxHeight: 600, child: SingleChildScrollView( diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 63275921d..c6769e68a 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -206,7 +206,8 @@ class _StepScaffoldState extends ConsumerState<StepScaffold> { void sendFromStack() { final trade = ref.read(desktopExchangeModelProvider)!.trade!; final address = trade.payInAddress; - final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency)!; + final coin = AppConfig.getCryptoCurrencyForTicker(trade.payInCurrency) ?? + AppConfig.getCryptoCurrencyByPrettyName(trade.payInCurrency); final amount = Decimal.parse(trade.payInAmount).toAmount( fractionDigits: coin.fractionDigits, ); diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart index 6c84996da..094ee452d 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/subwidgets/desktop_step_2.dart @@ -326,7 +326,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), )) CustomTextButton( - text: "Choose from Stack", + text: "Choose from ${AppConfig.prefix}", onTap: selectRecipientAddressFromStack, ), ], @@ -472,7 +472,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> { ), )) CustomTextButton( - text: "Choose from Stack", + text: "Choose from ${AppConfig.prefix}", onTap: selectRefundAddressFromStack, ), ], diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart index bbe22138e..1dec08ff8 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:tuple/tuple.dart'; +import '../../../app_config.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/amount/amount.dart'; @@ -87,7 +88,7 @@ class _DesktopChooseFromStackState crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Choose from Stack", + "Choose from ${AppConfig.prefix}", style: STextStyles.desktopH3(context), ), const SizedBox( diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index 81b99fd8f..9c9b9cc1d 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -11,16 +11,15 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../app_config.dart'; import '../providers/desktop/current_desktop_menu_item.dart'; +import '../providers/providers.dart'; import '../themes/stack_colors.dart'; import '../utilities/assets.dart'; import '../utilities/text_styles.dart'; -import '../wallets/crypto_currency/crypto_currency.dart'; import '../widgets/desktop/desktop_tor_status_button.dart'; import '../widgets/desktop/living_stack_icon.dart'; import 'desktop_menu_item.dart'; @@ -60,6 +59,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { late final DMIController torButtonController; double _width = expandedWidth; + bool get _isMinimized => _width < expandedWidth; void updateSelectedMenuItem(DesktopMenuItemId idKey) { widget.onSelectionWillChange?.call(idKey); @@ -114,6 +114,10 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { @override Widget build(BuildContext context) { + final prefs = ref.watch(prefsChangeNotifierProvider); + + final showExchange = prefs.enableExchange; + return Material( color: Theme.of(context).extension<StackColors>()!.popupBG, child: AnimatedContainer( @@ -163,7 +167,7 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { onPressed: () { ref.read(currentDesktopMenuItemProvider.state).state = DesktopMenuItemId.settings; - ref.watch(selectedSettingsMenuItemStateProvider.state).state = + ref.read(selectedSettingsMenuItemStateProvider.state).state = 4; }, ), @@ -181,114 +185,134 @@ class _DesktopMenuState extends ConsumerState<DesktopMenu> { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DesktopMenuItem( + key: const ValueKey('myStack'), duration: duration, icon: const DesktopMyStackIcon(), label: "My ${AppConfig.prefix}", value: DesktopMenuItemId.myStack, onChanged: updateSelectedMenuItem, controller: controllers[0], + isExpandedInitially: !_isMinimized, ), - if (AppConfig.hasFeature(AppFeature.swap)) + if (AppConfig.hasFeature(AppFeature.swap) && + showExchange) ...[ const SizedBox( height: 2, ), - if (AppConfig.hasFeature(AppFeature.swap)) DesktopMenuItem( + key: const ValueKey('swap'), duration: duration, icon: const DesktopExchangeIcon(), label: "Swap", value: DesktopMenuItemId.exchange, onChanged: updateSelectedMenuItem, controller: controllers[1], + isExpandedInitially: !_isMinimized, ), - if (AppConfig.hasFeature(AppFeature.buy)) + ], + if (AppConfig.hasFeature(AppFeature.buy) && + showExchange) ...[ const SizedBox( height: 2, ), - if (AppConfig.hasFeature(AppFeature.buy)) DesktopMenuItem( + key: const ValueKey('buy'), duration: duration, icon: const DesktopBuyIcon(), label: "Buy crypto", value: DesktopMenuItemId.buy, onChanged: updateSelectedMenuItem, controller: controllers[2], + isExpandedInitially: !_isMinimized, ), + ], const SizedBox( height: 2, ), DesktopMenuItem( + key: const ValueKey('notifications'), duration: duration, icon: const DesktopNotificationsIcon(), label: "Notifications", value: DesktopMenuItemId.notifications, onChanged: updateSelectedMenuItem, controller: controllers[3], + isExpandedInitially: !_isMinimized, ), const SizedBox( height: 2, ), DesktopMenuItem( + key: const ValueKey('addressBook'), duration: duration, icon: const DesktopAddressBookIcon(), label: "Address Book", value: DesktopMenuItemId.addressBook, onChanged: updateSelectedMenuItem, controller: controllers[4], + isExpandedInitially: !_isMinimized, ), const SizedBox( height: 2, ), DesktopMenuItem( + key: const ValueKey('settings'), duration: duration, icon: const DesktopSettingsIcon(), label: "Settings", value: DesktopMenuItemId.settings, onChanged: updateSelectedMenuItem, controller: controllers[5], + isExpandedInitially: !_isMinimized, ), const SizedBox( height: 2, ), DesktopMenuItem( + key: const ValueKey('support'), duration: duration, icon: const DesktopSupportIcon(), label: "Support", value: DesktopMenuItemId.support, onChanged: updateSelectedMenuItem, controller: controllers[6], + isExpandedInitially: !_isMinimized, ), const SizedBox( height: 2, ), DesktopMenuItem( + key: const ValueKey('about'), duration: duration, icon: const DesktopAboutIcon(), label: "About", value: DesktopMenuItemId.about, onChanged: updateSelectedMenuItem, controller: controllers[7], + isExpandedInitially: !_isMinimized, ), const Spacer(), if (!Platform.isIOS) DesktopMenuItem( + key: const ValueKey('exit'), duration: duration, labelLength: 123, icon: const DesktopExitIcon(), label: "Exit", value: 7, onChanged: (_) { - // todo: save stuff/ notify before exit? - if (AppConfig.coins - .where((e) => e is Monero || e is Wownero) - .isNotEmpty) { - // hack to insta kill because xmr/wow native lib code sucks - exit(0); - } else { - SystemNavigator.pop(); - } + // // todo: save stuff/ notify before exit? + // if (AppConfig.coins + // .where((e) => e is Monero || e is Wownero) + // .isNotEmpty) { + // // hack to insta kill because xmr/wow native lib code sucks + exit(0); + // } else { + // SystemNavigator.pop(); + // } }, controller: controllers[8], + isExpandedInitially: !_isMinimized, ), ], ), diff --git a/lib/pages_desktop_specific/desktop_menu_item.dart b/lib/pages_desktop_specific/desktop_menu_item.dart index 1fde1281c..ea0d69a81 100644 --- a/lib/pages_desktop_specific/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/desktop_menu_item.dart @@ -24,6 +24,9 @@ import 'desktop_menu.dart'; class DMIController { VoidCallback? toggle; + + DMIController(); + void dispose() { toggle = null; } @@ -237,6 +240,7 @@ class DesktopMenuItem<T> extends ConsumerStatefulWidget { required this.duration, this.labelLength = 125, this.controller, + required this.isExpandedInitially, }); final Widget icon; @@ -246,6 +250,7 @@ class DesktopMenuItem<T> extends ConsumerStatefulWidget { final Duration duration; final double labelLength; final DMIController? controller; + final bool isExpandedInitially; @override ConsumerState<DesktopMenuItem<T>> createState() => _DesktopMenuItemState<T>(); @@ -287,11 +292,17 @@ class _DesktopMenuItemState<T> extends ConsumerState<DesktopMenuItem<T>> labelLength = widget.labelLength; controller = widget.controller; + _iconOnly = !widget.isExpandedInitially; controller?.toggle = toggle; animationController = AnimationController( vsync: this, duration: duration, - )..forward(); + ); + if (_iconOnly) { + animationController.value = 0; + } else { + animationController.value = 1; + } super.initState(); } diff --git a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart index 5246a6b16..aa3244ece 100644 --- a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart +++ b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_expanding_wallet_card.dart @@ -11,19 +11,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; -import '../../../models/isar/models/ethereum/eth_contract.dart'; + +import '../../../pages/wallets_view/wallets_overview.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/text_styles.dart'; -import '../../../wallets/wallet/wallet.dart'; import '../../../widgets/animated_widgets/rotate_icon.dart'; import '../../../widgets/expandable.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/wallet_card.dart'; import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_balance.dart'; import '../../../widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; -import 'package:tuple/tuple.dart'; class DesktopExpandingWalletCard extends StatefulWidget { const DesktopExpandingWalletCard({ @@ -32,7 +31,7 @@ class DesktopExpandingWalletCard extends StatefulWidget { required this.navigatorState, }); - final Tuple2<Wallet, List<EthContract>> data; + final WalletListItemData data; final NavigatorState navigatorState; @override @@ -48,9 +47,9 @@ class _DesktopExpandingWalletCardState @override void initState() { - if (widget.data.item1.cryptoCurrency.hasTokenSupport) { + if (widget.data.wallet.cryptoCurrency.hasTokenSupport) { tokenContractAddresses.addAll( - widget.data.item2.map((e) => e.address), + widget.data.contracts.map((e) => e.address), ); } @@ -63,7 +62,7 @@ class _DesktopExpandingWalletCardState padding: EdgeInsets.zero, borderColor: Theme.of(context).extension<StackColors>()!.backgroundAppBar, child: Expandable( - initialState: widget.data.item1.cryptoCurrency.hasTokenSupport + initialState: widget.data.wallet.cryptoCurrency.hasTokenSupport ? ExpandableState.expanded : ExpandableState.collapsed, controller: expandableController, @@ -89,13 +88,13 @@ class _DesktopExpandingWalletCardState child: Row( children: [ WalletInfoCoinIcon( - coin: widget.data.item1.info.coin, + coin: widget.data.wallet.info.coin, ), const SizedBox( width: 12, ), Text( - widget.data.item1.info.name, + widget.data.wallet.info.name, style: STextStyles.desktopTextExtraSmall(context) .copyWith( color: Theme.of(context) @@ -109,7 +108,7 @@ class _DesktopExpandingWalletCardState Expanded( flex: 4, child: WalletInfoRowBalance( - walletId: widget.data.item1.walletId, + walletId: widget.data.wallet.walletId, ), ), ], @@ -173,7 +172,7 @@ class _DesktopExpandingWalletCardState bottom: 14, ), child: SimpleWalletCard( - walletId: widget.data.item1.walletId, + walletId: widget.data.wallet.walletId, popPrevious: true, desktopNavigatorState: widget.navigatorState, ), @@ -187,7 +186,7 @@ class _DesktopExpandingWalletCardState bottom: 14, ), child: SimpleWalletCard( - walletId: widget.data.item1.walletId, + walletId: widget.data.wallet.walletId, contractAddress: e, popPrevious: true, desktopNavigatorState: widget.navigatorState, 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 50c76ab7e..a792641ba 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 @@ -182,7 +182,9 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { final wallet = ref.read(pWallets).getWallet(walletId); supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface; showMultiType = supportsSpark || - ref.read(pWallets).getWallet(walletId) is MultiAddressInterface; + (wallet is! BCashInterface && + wallet is Bip39HDWallet && + wallet.supportedAddressTypes.length > 1); _walletAddressTypes.add(wallet.info.mainAddressType); @@ -338,7 +340,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> { onTap: () { clipboard.setData( ClipboardData( - text: ref.watch(pWalletReceivingAddress(walletId)), + text: address, ), ); showFloatingFlushBar( 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 0691cc9de..4d9eaf026 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 @@ -353,6 +353,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> { final wallet = ref.watch(pWallets).getWallet(widget.walletId); final coin = wallet.info.coin; + final prefs = ref.watch(prefsChangeNotifierProvider); + final showExchange = prefs.enableExchange; + final showMore = wallet is PaynymInterface || (wallet is CoinControlInterface && ref.watch( @@ -368,7 +371,9 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> { return Row( children: [ - if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.swap)) + if (Constants.enableExchange && + AppConfig.hasFeature(AppFeature.swap) && + showExchange) SecondaryButton( label: "Swap", width: buttonWidth, @@ -383,11 +388,15 @@ class _DesktopWalletFeaturesState extends ConsumerState<DesktopWalletFeatures> { ), onPressed: () => _onSwapPressed(), ), - if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.buy)) + if (Constants.enableExchange && + AppConfig.hasFeature(AppFeature.buy) && + showExchange) const SizedBox( width: 16, ), - if (Constants.enableExchange && AppConfig.hasFeature(AppFeature.buy)) + if (Constants.enableExchange && + AppConfig.hasFeature(AppFeature.buy) && + showExchange) SecondaryButton( label: "Buy", width: buttonWidth, 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 76fa097a4..0eef9750d 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 @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import '../../../../../app_config.dart'; import '../../../../../db/sqlite/firo_cache.dart'; import '../../../../../providers/db/main_db_provider.dart'; import '../../../../../providers/global/prefs_provider.dart'; @@ -19,6 +20,7 @@ import '../../../../../providers/global/wallets_provider.dart'; import '../../../../../themes/stack_colors.dart'; import '../../../../../utilities/assets.dart'; import '../../../../../utilities/text_styles.dart'; +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'; @@ -32,6 +34,8 @@ import '../../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.da import '../../../../../widgets/custom_buttons/draggable_switch_button.dart'; import '../../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../../../widgets/desktop/primary_button.dart'; +import '../../../../../widgets/desktop/secondary_button.dart'; import '../../../../../widgets/rounded_container.dart'; class MoreFeaturesDialog extends ConsumerStatefulWidget { @@ -102,6 +106,117 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> { } } + bool _switchReuseAddressToggledLock = false; // Mutex. + Future<void> _switchReuseAddressToggled(bool newValue) async { + if (newValue) { + await showDialog( + context: context, + builder: (context) { + final isDesktop = Util.isDesktop; + return DesktopDialog( + maxWidth: 576, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Warning!", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 32, + right: 32, + bottom: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Reusing addresses reduces your privacy and security. Are you sure you want to reuse addresses by default?", + style: STextStyles.desktopTextSmall(context), + ), + const SizedBox( + height: 43, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(false); + }, + label: "Cancel", + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(true); + }, + label: "Continue", + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ).then((confirmed) async { + if (_switchReuseAddressToggledLock) { + return; + } + _switchReuseAddressToggledLock = true; // Lock mutex. + + try { + if (confirmed == true) { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: true, + }, + isar: ref.read(mainDBProvider).isar, + ); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } finally { + // ensure _switchReuseAddressToggledLock is set to false no matter what. + _switchReuseAddressToggledLock = false; + } + }); + } else { + await ref.read(pWalletInfo(widget.walletId)).updateOtherData( + newEntries: { + WalletInfoKeys.reuseAddress: false, + }, + isar: ref.read(mainDBProvider).isar, + ); + } + } + @override Widget build(BuildContext context) { final wallet = ref.watch( @@ -167,7 +282,7 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> { if (wallet is OrdinalsInterface) _MoreFeaturesItem( label: "Ordinals", - detail: "View and control your ordinals in Stack", + detail: "View and control your ordinals in ${AppConfig.prefix}", iconAsset: Assets.svg.ordinal, onPressed: () async => widget.onOrdinalsPressed?.call(), ), @@ -253,6 +368,38 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> { ], ), ), + // reuseAddress preference. + _MoreFeaturesItemBase( + child: Row( + children: [ + const SizedBox(width: 3), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + pWalletInfo(widget.walletId) + .select((value) => value.otherData), + )[WalletInfoKeys.reuseAddress] as bool? ?? + false, + onValueChanged: _switchReuseAddressToggled, + ), + ), + const SizedBox( + width: 16, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Reuse receiving address", + style: STextStyles.w600_20(context), + ), + ], + ), + ], + ), + ), const SizedBox( height: 28, ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart index 6500b669e..e332c4543 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../../models/keys/key_data_interface.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../providers/desktop/storage_crypto_handler_provider.dart'; import '../../../../providers/providers.dart'; @@ -22,6 +23,7 @@ import '../../../../utilities/assets.dart'; import '../../../../utilities/constants.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; +import '../../../../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; @@ -85,7 +87,6 @@ class _UnlockWalletKeysDesktopState final wallet = ref.read(pWallets).getWallet(widget.walletId); ({String keys, String config})? frostData; List<String>? words; - ({List<XPriv> xprivs, String fingerprint})? xprivData; // TODO: [prio=low] handle wallets that don't have a mnemonic // All wallets currently are mnemonic based @@ -102,8 +103,11 @@ class _UnlockWalletKeysDesktopState words = await wallet.getMnemonicAsWords(); } + KeyDataInterface? keyData; if (wallet is ExtendedKeysInterface) { - xprivData = await wallet.getXPrivs(); + keyData = await wallet.getXPrivs(); + } else if (wallet is CwBasedInterface) { + keyData = await wallet.getKeys(); } if (mounted) { @@ -113,7 +117,7 @@ class _UnlockWalletKeysDesktopState mnemonic: words ?? [], walletId: widget.walletId, frostData: frostData, - xprivData: xprivData, + keyData: keyData, ), ); } @@ -327,10 +331,6 @@ class _UnlockWalletKeysDesktopState ({String keys, String config})? frostData; List<String>? words; - ({ - List<XPriv> xprivs, - String fingerprint - })? xprivData; final wallet = ref.read(pWallets).getWallet(widget.walletId); @@ -350,11 +350,14 @@ class _UnlockWalletKeysDesktopState words = await wallet.getMnemonicAsWords(); } + KeyDataInterface? keyData; if (wallet is ExtendedKeysInterface) { - xprivData = await wallet.getXPrivs(); + keyData = await wallet.getXPrivs(); + } else if (wallet is CwBasedInterface) { + keyData = await wallet.getKeys(); } - if (mounted) { + if (context.mounted) { await Navigator.of(context) .pushReplacementNamed( WalletKeysDesktopPopup.routeName, @@ -362,7 +365,7 @@ class _UnlockWalletKeysDesktopState mnemonic: words ?? [], walletId: widget.walletId, frostData: frostData, - xprivData: xprivData, + keyData: keyData, ), ); } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart index a3ec985da..f1bc2d9f6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart @@ -14,8 +14,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../models/keys/cw_key_data.dart'; +import '../../../../models/keys/key_data_interface.dart'; +import '../../../../models/keys/xpriv_data.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart'; +import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/cn_wallet_keys.dart'; import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart'; import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart'; import '../../../../themes/stack_colors.dart'; @@ -23,7 +27,6 @@ import '../../../../utilities/address_utils.dart'; import '../../../../utilities/assets.dart'; import '../../../../utilities/clipboard_interface.dart'; import '../../../../utilities/text_styles.dart'; -import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../../widgets/custom_tab_view.dart'; import '../../../../widgets/desktop/desktop_dialog.dart'; import '../../../../widgets/desktop/desktop_dialog_close_button.dart'; @@ -39,14 +42,14 @@ class WalletKeysDesktopPopup extends ConsumerWidget { required this.walletId, this.frostData, this.clipboardInterface = const ClipboardWrapper(), - this.xprivData, + this.keyData, }); final List<String> words; final String walletId; final ({String keys, String config})? frostData; final ClipboardInterface clipboardInterface; - final ({List<XPriv> xprivs, String fingerprint})? xprivData; + final KeyDataInterface? keyData; static const String routeName = "walletKeysDesktopPopup"; @@ -176,9 +179,13 @@ class WalletKeysDesktopPopup extends ConsumerWidget { ), ], ) - : xprivData != null + : keyData != null ? CustomTabView( - titles: const ["Mnemonic", "XPriv(s)"], + titles: [ + "Mnemonic", + if (keyData is XPrivData) "XPriv(s)", + if (keyData is CWKeyData) "Keys", + ], children: [ Padding( padding: const EdgeInsets.only(top: 16), @@ -186,10 +193,16 @@ class WalletKeysDesktopPopup extends ConsumerWidget { words: words, ), ), - WalletXPrivs( - xprivData: xprivData!, - walletId: walletId, - ), + if (keyData is XPrivData) + WalletXPrivs( + xprivData: keyData as XPrivData, + walletId: walletId, + ), + if (keyData is CWKeyData) + CNWalletKeys( + cwKeyData: keyData as CWKeyData, + walletId: walletId, + ), ], ) : _Mnemonic( @@ -264,22 +277,6 @@ class _Mnemonic extends StatelessWidget { children: [ Expanded( child: SecondaryButton( - label: "Show QR code", - onPressed: () { - // TODO: address utils - final String value = AddressUtils.encodeQRSeedData(words); - Navigator.of(context).pushNamed( - QRCodeDesktopPopupContent.routeName, - arguments: value, - ); - }, - ), - ), - const SizedBox( - width: 16, - ), - Expanded( - child: PrimaryButton( label: "Copy", onPressed: () async { await clipboardInterface.setData( @@ -298,6 +295,22 @@ class _Mnemonic extends StatelessWidget { }, ), ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Show QR code", + onPressed: () { + // TODO: address utils + final String value = AddressUtils.encodeQRSeedData(words); + Navigator.of(context).pushNamed( + QRCodeDesktopPopupContent.routeName, + arguments: value, + ); + }, + ), + ), ], ), ), diff --git a/lib/pages_desktop_specific/password/delete_password_warning_view.dart b/lib/pages_desktop_specific/password/delete_password_warning_view.dart index 1eab70a5a..e8a4c5256 100644 --- a/lib/pages_desktop_specific/password/delete_password_warning_view.dart +++ b/lib/pages_desktop_specific/password/delete_password_warning_view.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; +import '../../app_config.dart'; import '../../db/hive/db.dart'; import '../../notifications/show_flush_bar.dart'; import '../../pages/intro_view.dart'; @@ -143,7 +144,7 @@ class _ForgotPasswordDesktopViewState ), TextSpan( text: widget.shouldCreateNew - ? "create a new Stack" + ? "create a new ${AppConfig.prefix}" : "restore from backup", style: STextStyles.desktopTextSmallBold(context), ), diff --git a/lib/pages_desktop_specific/password/forgot_password_desktop_view.dart b/lib/pages_desktop_specific/password/forgot_password_desktop_view.dart index 235f14563..68cf1476f 100644 --- a/lib/pages_desktop_specific/password/forgot_password_desktop_view.dart +++ b/lib/pages_desktop_specific/password/forgot_password_desktop_view.dart @@ -74,7 +74,9 @@ class _ForgotPasswordDesktopViewState SizedBox( width: 400, child: Text( - "${AppConfig.appName} does not store your password. Create new wallet or use a Stack backup file to restore your wallet.", + "${AppConfig.appName} does not store your password. " + "Create new wallet or use a ${AppConfig.prefix} " + "backup file to restore your wallet.", textAlign: TextAlign.center, style: STextStyles.desktopTextSmall(context).copyWith( color: Theme.of(context) @@ -87,7 +89,7 @@ class _ForgotPasswordDesktopViewState height: 48, ), PrimaryButton( - label: "Create new Stack", + label: "Create new ${AppConfig.prefix}", onPressed: () { const shouldCreateNew = true; Navigator.of(context).pushNamed( @@ -100,7 +102,7 @@ class _ForgotPasswordDesktopViewState height: 24, ), SecondaryButton( - label: "Restore from Stack backup", + label: "Restore from ${AppConfig.prefix} backup", onPressed: () { const shouldCreateNew = false; Navigator.of(context).pushNamed( diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index 173840528..a20660779 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -94,7 +94,7 @@ class _ForgottenPassphraseRestoreFromSWBState color: Colors.transparent, child: Center( child: Text( - "Decrypting Stack backup file", + "Decrypting ${AppConfig.prefix} backup file", style: STextStyles.pageTitleH2(context).copyWith( color: Theme.of(context).extension<StackColors>()!.textWhite, @@ -245,7 +245,7 @@ class _ForgottenPassphraseRestoreFromSWBState height: 32, ), Text( - "Use your Stack wallet backup file to restore your wallets, address book, and wallet preferences.", + "Use your ${AppConfig.prefix} backup file to restore your wallets, address book, and wallet preferences.", textAlign: TextAlign.center, style: STextStyles.desktopTextSmall(context).copyWith( color: Theme.of(context) diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart index 029c5e294..2fbefe90b 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/advanced_settings.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../../app_config.dart'; import '../../../../pages/settings_views/global_settings_view/advanced_views/manage_coin_units/manage_coin_units_view.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../themes/stack_colors.dart'; @@ -161,6 +162,47 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { ], ), ), + // showExchange pref. + const Padding( + padding: EdgeInsets.all(10.0), + child: Divider( + thickness: 0.5, + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable exchange features", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension<StackColors>()! + .textDark, + ), + textAlign: TextAlign.left, + ), + SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableExchange, + ), + ), + onValueChanged: (newValue) { + ref + .read(prefsChangeNotifierProvider) + .enableExchange = newValue; + }, + ), + ), + ], + ), + ), const Padding( padding: EdgeInsets.all(10.0), child: Divider( @@ -184,7 +226,7 @@ class _AdvancedSettings extends ConsumerState<AdvancedSettings> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Stack Experience", + "${AppConfig.prefix} Experience", style: STextStyles.desktopTextExtraSmall( context, ).copyWith( diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_manage_block_explorers_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_manage_block_explorers_dialog.dart index e69f90833..41e65e9d7 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_manage_block_explorers_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/desktop_manage_block_explorers_dialog.dart @@ -226,9 +226,9 @@ class _DesktopEditBlockExplorerDialogState " every block explorer has a slightly different URL scheme." "\n\n" "Paste in your block explorer of choice, then edit in" - " [TXID] where the transaction ID should go, and Stack" - " Wallet will auto fill the transaction ID in that place" - " of the URL.", + " [TXID] where the transaction ID should go, and " + "${AppConfig.appName} will auto fill the transaction" + " ID in that place of the URL.", style: STextStyles.desktopTextExtraExtraSmall(context), ), ), diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart index 20a9d9e62..1cba43b0a 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -15,6 +15,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../../app_config.dart'; import '../../../../db/hive/db.dart'; import '../../../../providers/global/prefs_provider.dart'; import '../../../../providers/global/price_provider.dart'; @@ -69,7 +70,7 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> { Padding( padding: const EdgeInsets.all(32), child: Text( - "Choose Your Stack Experience", + "Choose Your ${AppConfig.prefix} Experience", style: STextStyles.desktopH3(context), textAlign: TextAlign.center, ), @@ -192,9 +193,11 @@ class _StackPrivacyDialog extends ConsumerState<StackPrivacyDialog> { ) .then((_) { if (isEasy) { - unawaited( - ExchangeDataLoadingService.instance.loadAll(), - ); + if (AppConfig.hasFeature(AppFeature.swap)) { + unawaited( + ExchangeDataLoadingService.instance.loadAll(), + ); + } ref .read(priceAnd24hChangeNotifierProvider) .start(true); diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart index e7225d628..bf5f3e009 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart @@ -20,6 +20,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; import 'package:zxcvbn/zxcvbn.dart'; +import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import '../../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; @@ -776,7 +777,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { if (Platform.isAndroid) { return StackOkDialog( title: - "Stack Auto Backup enabled and saved to:", + "${AppConfig.prefix} Auto Backup enabled and saved to:", message: fileToSave, ); } else if (Util.isDesktop) { @@ -800,7 +801,7 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { .spaceBetween, children: [ Text( - "Stack Auto Backup enabled!", + "${AppConfig.prefix} Auto Backup enabled!", style: STextStyles.desktopH3( context, @@ -834,7 +835,8 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> { ); } else { return const StackOkDialog( - title: "Stack Auto Backup enabled!", + title: + "${AppConfig.prefix} Auto Backup enabled!", ); } }, diff --git a/lib/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart b/lib/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart index a384ffacf..5b84bb812 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/syncing_preferences_settings.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import '../../../app_config.dart'; import '../../../pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import '../../../providers/global/prefs_provider.dart'; import '../../../themes/stack_colors.dart'; @@ -115,7 +116,7 @@ class _SyncingPreferencesSettings ), TextSpan( text: - "\n\nSet up your syncing preferences for all wallets in your Stack.", + "\n\nSet up your syncing preferences for all wallets in your ${AppConfig.prefix}.", style: STextStyles.desktopTextExtraExtraSmall( context, ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index fe7b36872..b449525b6 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -22,6 +22,7 @@ import 'models/isar/models/blockchain_data/v2/transaction_v2.dart'; import 'models/isar/models/contact_entry.dart'; import 'models/isar/models/isar_models.dart'; import 'models/isar/ordinal.dart'; +import 'models/keys/key_data_interface.dart'; import 'models/paynym/paynym_account_lite.dart'; import 'models/send_view_auto_fill_data.dart'; import 'pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; @@ -1280,14 +1281,14 @@ class RouteGenerator { } else if (args is ({ String walletId, List<String> mnemonic, - ({List<XPriv> xprivs, String fingerprint})? xprivData, + KeyDataInterface? keyData, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => WalletBackupView( walletId: args.walletId, mnemonic: args.mnemonic, - xprivData: args.xprivData, + keyData: args.keyData, ), settings: RouteSettings( name: settings.name, @@ -1296,7 +1297,7 @@ class RouteGenerator { } else if (args is ({ String walletId, List<String> mnemonic, - ({List<XPriv> xprivs, String fingerprint})? xprivData, + KeyDataInterface? keyData, ({ String myName, String config, @@ -1310,7 +1311,7 @@ class RouteGenerator { walletId: args.walletId, mnemonic: args.mnemonic, frostWalletData: args.frostWalletData, - xprivData: args.xprivData, + keyData: args.keyData, ), settings: RouteSettings( name: settings.name, @@ -1319,16 +1320,16 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case MobileXPrivsView.routeName: + case MobileKeyDataView.routeName: if (args is ({ String walletId, - ({List<XPriv> xprivs, String fingerprint}) xprivData, + KeyDataInterface keyData, })) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MobileXPrivsView( + builder: (_) => MobileKeyDataView( walletId: args.walletId, - xprivData: args.xprivData, + keyData: args.keyData, ), settings: RouteSettings( name: settings.name, @@ -2369,14 +2370,14 @@ class RouteGenerator { List<String> mnemonic, String walletId, ({String keys, String config})? frostData, - ({List<XPriv> xprivs, String fingerprint})? xprivData, + KeyDataInterface? keyData, })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, frostData: args.frostData, - xprivData: args.xprivData, + keyData: args.keyData, ), RouteSettings( name: settings.name, @@ -2385,13 +2386,13 @@ class RouteGenerator { } else if (args is ({ List<String> mnemonic, String walletId, - ({List<XPriv> xprivs, String fingerprint})? xprivData, + KeyDataInterface? keyData, })) { return FadePageRoute( WalletKeysDesktopPopup( words: args.mnemonic, walletId: args.walletId, - xprivData: args.xprivData, + keyData: args.keyData, ), RouteSettings( name: settings.name, diff --git a/lib/services/debug_service.dart b/lib/services/debug_service.dart index 189c4f19f..4d6e51553 100644 --- a/lib/services/debug_service.dart +++ b/lib/services/debug_service.dart @@ -15,6 +15,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; +import '../app_config.dart'; import '../models/isar/models/log.dart'; import '../utilities/logger.dart'; @@ -98,7 +99,7 @@ class DebugService extends ChangeNotifier { Future<String> exportToFile(String directory, EventBus eventBus) async { final now = DateTime.now(); final filename = - "Stack_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt"; + "${AppConfig.prefix}_Wallet_logs_${now.year}_${now.month}_${now.day}_${now.hour}_${now.minute}_${now.second}.txt"; final filepath = "$directory/$filename"; final File file = await File(filepath).create(); diff --git a/lib/services/event_bus/events/wallet_added_event.dart b/lib/services/event_bus/events/wallet_added_event.dart new file mode 100644 index 000000000..0407946fc --- /dev/null +++ b/lib/services/event_bus/events/wallet_added_event.dart @@ -0,0 +1 @@ +class WalletAddedEvent {} diff --git a/lib/services/frost.dart b/lib/services/frost.dart index 632299e37..06eb674f6 100644 --- a/lib/services/frost.dart +++ b/lib/services/frost.dart @@ -284,7 +284,13 @@ abstract class Frost { static String createSignConfig({ required int network, - required List<({UTXO utxo, Uint8List scriptPubKey})> inputs, + required List< + ({ + UTXO utxo, + Uint8List scriptPubKey, + AddressDerivationData addressDerivationData + })> + inputs, required List<({String address, Amount amount, bool isChange})> outputs, required String changeAddress, required int feePerWeight, @@ -299,6 +305,7 @@ abstract class Frost { vout: e.utxo.vout, value: e.utxo.value, scriptPubKey: e.scriptPubKey, + addressDerivationData: e.addressDerivationData, ), ) .toList(), diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index a5a62c188..13dec90d7 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -8,13 +8,16 @@ * */ +import 'dart:async'; + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../models/notification_model.dart'; import '../utilities/prefs.dart'; import 'notifications_service.dart'; -class NotificationApi { +abstract final class NotificationApi { + static Completer<void>? _initCalledCompleter; static final _notifications = FlutterLocalNotificationsPlugin(); // static final onNotifications = BehaviorSubject<String?>(); @@ -33,6 +36,16 @@ class NotificationApi { } static Future<void> init({bool initScheduled = false}) async { + if (_initCalledCompleter == null) { + _initCalledCompleter = Completer<void>(); + } else { + if (_initCalledCompleter!.isCompleted) { + return; + } else { + return await _initCalledCompleter!.future; + } + } + const android = AndroidInitializationSettings('app_icon_alpha'); const iOS = DarwinInitializationSettings(); const linux = LinuxInitializationSettings( @@ -54,12 +67,18 @@ class NotificationApi { // onNotifications.add(payload.payload); // }, ); + _initCalledCompleter!.complete(); } - static Future<void> clearNotifications() async => _notifications.cancelAll(); + static Future<void> clearNotifications() async { + await init(); + await _notifications.cancelAll(); + } - static Future<void> clearNotification(int id) async => - _notifications.cancel(id); + static Future<void> clearNotification(int id) async { + await init(); + await _notifications.cancel(id); + } //=================================== static late Prefs prefs; @@ -79,6 +98,7 @@ class NotificationApi { String? changeNowId, String? payload, }) async { + await init(); await prefs.incrementCurrentNotificationIndex(); final id = prefs.currentNotificationId; diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 43f0af396..c4e75bde8 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -13,12 +13,10 @@ import 'dart:async'; import 'package:flutter_libmonero/monero/monero.dart' as monero; import 'package:flutter_libmonero/wownero/wownero.dart' as wownero; import 'package:isar/isar.dart'; + +import '../app_config.dart'; import '../db/hive/db.dart'; import '../db/isar/main_db.dart'; -import 'node_service.dart'; -import 'notifications_service.dart'; -import 'trade_sent_from_stack_service.dart'; -import '../app_config.dart'; import '../utilities/enums/sync_type_enum.dart'; import '../utilities/flutter_secure_storage_interface.dart'; import '../utilities/logger.dart'; @@ -28,6 +26,11 @@ import '../wallets/isar/models/wallet_info.dart'; import '../wallets/wallet/impl/epiccash_wallet.dart'; import '../wallets/wallet/wallet.dart'; import '../wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart'; +import 'event_bus/events/wallet_added_event.dart'; +import 'event_bus/global_event_bus.dart'; +import 'node_service.dart'; +import 'notifications_service.dart'; +import 'trade_sent_from_stack_service.dart'; class Wallets { Wallets._private(); @@ -59,6 +62,7 @@ class Wallets { ); } _wallets[wallet.walletId] = wallet; + GlobalEventBus.instance.fire(WalletAddedEvent()); } Future<void> deleteWallet( diff --git a/lib/themes/theme_service.dart b/lib/themes/theme_service.dart index cf4528719..0e3ff68d1 100644 --- a/lib/themes/theme_service.dart +++ b/lib/themes/theme_service.dart @@ -31,7 +31,9 @@ final pThemeService = Provider<ThemeService>((ref) { }); class ThemeService { - static const _currentDefaultThemeVersion = 10; + // dumb quick conditional based on name. Should really be done better + static const _currentDefaultThemeVersion = + AppConfig.appName == "Campfire" ? 17 : 15; ThemeService._(); static ThemeService? _instance; static ThemeService get instance => _instance ??= ThemeService._(); diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index 93872aeef..da12b825c 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -8,11 +8,12 @@ * */ +import '../app_config.dart'; import '../wallets/crypto_currency/crypto_currency.dart'; abstract class DefaultNodes { static const String defaultNodeIdPrefix = "default_"; static String buildId(CryptoCurrency cryptoCurrency) => "$defaultNodeIdPrefix${cryptoCurrency.identifier}"; - static const String defaultName = "Stack Default"; + static const String defaultName = "${AppConfig.prefix} Default"; } diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 78a461ec7..5ffc7a059 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -71,6 +71,7 @@ class Prefs extends ChangeNotifier { _useTor = await _getUseTor(); _fusionServerInfo = await _getFusionServerInfo(); _autoPin = await _getAutoPin(); + _enableExchange = await _getEnableExchange(); _initialized = true; } @@ -1131,4 +1132,30 @@ class Prefs extends ChangeNotifier { ) as bool? ?? false; } + + // Show or hide exchange (buy & swap) features. + + bool _enableExchange = true; + + bool get enableExchange => _enableExchange; + + set enableExchange(bool showExchange) { + if (_enableExchange != showExchange) { + DB.instance.put<dynamic>( + boxName: DB.boxNamePrefs, + key: "showExchange", + value: showExchange, + ); + _enableExchange = showExchange; + notifyListeners(); + } + } + + Future<bool> _getEnableExchange() async { + return await DB.instance.get<dynamic>( + boxName: DB.boxNamePrefs, + key: "showExchange", + ) as bool? ?? + true; + } } diff --git a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart index f09d1865e..64c90a719 100644 --- a/lib/wallets/crypto_currency/coins/bitcoin_frost.dart +++ b/lib/wallets/crypto_currency/coins/bitcoin_frost.dart @@ -127,9 +127,21 @@ class BitcoinFrost extends FrostCurrency { ); @override - String pubKeyToScriptHash({required Uint8List pubKey}) { + Uint8List addressToPubkey({required String address}) { try { - return Bip39HDCurrency.convertBytesToScriptHash(pubKey); + final addr = coinlib.Address.fromString(address, networkParams); + return addr.program.script.compiled; + } catch (e) { + rethrow; + } + } + + @override + String addressToScriptHash({required String address}) { + try { + return Bip39HDCurrency.convertBytesToScriptHash( + addressToPubkey(address: address), + ); } catch (e) { rethrow; } diff --git a/lib/wallets/crypto_currency/intermediate/frost_currency.dart b/lib/wallets/crypto_currency/intermediate/frost_currency.dart index fd452bb62..c39e5cbce 100644 --- a/lib/wallets/crypto_currency/intermediate/frost_currency.dart +++ b/lib/wallets/crypto_currency/intermediate/frost_currency.dart @@ -6,7 +6,11 @@ import '../crypto_currency.dart'; abstract class FrostCurrency extends CryptoCurrency { FrostCurrency(super.network); - String pubKeyToScriptHash({required Uint8List pubKey}); + // String pubKeyToScriptHash({required Uint8List pubKey}); + + String addressToScriptHash({required String address}); + + Uint8List addressToPubkey({required String address}); Amount get dustLimit; } diff --git a/lib/wallets/isar/models/wallet_info.dart b/lib/wallets/isar/models/wallet_info.dart index 6f6e12a42..6a7a9b54c 100644 --- a/lib/wallets/isar/models/wallet_info.dart +++ b/lib/wallets/isar/models/wallet_info.dart @@ -511,4 +511,5 @@ abstract class WalletInfoKeys { static const String firoSparkCacheSetTimestampCache = "firoSparkCacheSetTimestampCacheKey"; static const String enableOptInRbf = "enableOptInRbfKey"; + static const String reuseAddress = "reuseAddressKey"; } diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 774d85e06..99a28a138 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -133,21 +133,41 @@ class TxData { .reduce((total, amount) => total += amount) : null; - Amount? get amountWithoutChange => - recipients != null && recipients!.isNotEmpty - ? recipients! - .where((e) => !e.isChange) - .map((e) => e.amount) - .reduce((total, amount) => total += amount) - : null; + Amount? get amountWithoutChange { + if (recipients != null && recipients!.isNotEmpty) { + if (recipients!.where((e) => !e.isChange).isEmpty) { + return Amount( + rawValue: BigInt.zero, + fractionDigits: recipients!.first.amount.fractionDigits, + ); + } else { + return recipients! + .where((e) => !e.isChange) + .map((e) => e.amount) + .reduce((total, amount) => total += amount); + } + } else { + return null; + } + } - Amount? get amountSparkWithoutChange => - sparkRecipients != null && sparkRecipients!.isNotEmpty - ? sparkRecipients! - .where((e) => !e.isChange) - .map((e) => e.amount) - .reduce((total, amount) => total += amount) - : null; + Amount? get amountSparkWithoutChange { + if (sparkRecipients != null && sparkRecipients!.isNotEmpty) { + if (sparkRecipients!.where((e) => !e.isChange).isEmpty) { + return Amount( + rawValue: BigInt.zero, + fractionDigits: sparkRecipients!.first.amount.fractionDigits, + ); + } else { + return sparkRecipients! + .where((e) => !e.isChange) + .map((e) => e.amount) + .reduce((total, amount) => total += amount); + } + } else { + return null; + } + } int? get estimatedSatsPerVByte => fee != null && vSize != null ? (fee!.raw ~/ BigInt.from(vSize!)).toInt() diff --git a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart index d41d3d4c2..24a690b2a 100644 --- a/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart +++ b/lib/wallets/wallet/impl/bitcoin_frost_wallet.dart @@ -1,10 +1,13 @@ import 'dart:async'; import 'dart:ffi'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:frostdart/frostdart.dart' as frost; import 'package:frostdart/frostdart_bindings_generated.dart'; +import 'package:frostdart/util.dart'; import 'package:isar/isar.dart'; + import '../../../electrumx_rpc/cached_electrumx_client.dart'; import '../../../electrumx_rpc/electrumx_client.dart'; import '../../../models/balance.dart'; @@ -24,10 +27,13 @@ import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/intermediate/frost_currency.dart'; import '../../isar/models/frost_wallet_info.dart'; +import '../../isar/models/wallet_info.dart'; import '../../models/tx_data.dart'; import '../wallet.dart'; +import '../wallet_mixin_interfaces/multi_address_interface.dart'; -class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { +class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> + with MultiAddressInterface { BitcoinFrostWallet(CryptoCurrencyNetwork network) : super(BitcoinFrost(network) as T); @@ -77,25 +83,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { await mainDB.isar.frostWalletInfo.put(frostWalletInfo); }); - final keys = frost.deserializeKeys(keys: serializedKeys); - - final addressString = frost.addressForKeys( - network: cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet, - keys: keys, - ); - - final publicKey = frost.scriptPubKeyForKeys(keys: keys); - - final address = Address( - walletId: info.walletId, - value: addressString, - publicKey: publicKey.toUint8ListFromHex, - derivationIndex: 0, - derivationPath: null, - subType: AddressSubType.receiving, - type: AddressType.unknown, + final address = await _generateAddress( + change: 0, + index: 0, + serializedKeys: serializedKeys, ); await mainDB.putAddresses([address]); @@ -110,7 +101,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { Future<TxData> frostCreateSignConfig({ required TxData txData, - required String changeAddress, required int feePerWeight, }) async { try { @@ -152,44 +142,86 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { fractionDigits: cryptoCurrency.fractionDigits, ); final Set<UTXO> utxosToUse = {}; - for (final utxo in utxos) { + final Set<UTXO> utxosRemaining = {}; + for (int i = 0; i < utxos.length; i++) { + final utxo = utxos[i]; sum += Amount( rawValue: BigInt.from(utxo.value), fractionDigits: cryptoCurrency.fractionDigits, ); utxosToUse.add(utxo); if (sum > total) { + if (i + 1 < utxos.length) { + utxosRemaining.addAll(utxos.sublist(i)); + } break; } } - final serializedKeys = await getSerializedKeys(); - final keys = frost.deserializeKeys(keys: serializedKeys!); - final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main ? Network.Mainnet : Network.Testnet; - final publicKey = frost - .scriptPubKeyForKeys( - keys: keys, - ) - .toUint8ListFromHex; + final List< + ({ + UTXO utxo, + Uint8List scriptPubKey, + ({int account, int index, bool change}) addressDerivationData + })> inputs = []; - final config = Frost.createSignConfig( - network: network, - inputs: utxosToUse - .map( - (e) => ( - utxo: e, + for (final utxo in utxosToUse) { + final dData = await getDerivationData( + utxo.address, + ); + final publicKey = cryptoCurrency.addressToPubkey( + address: utxo.address!, + ); + + inputs.add( + ( + utxo: utxo, + scriptPubKey: publicKey, + addressDerivationData: dData, + ), + ); + } + await checkChangeAddressForTransactions(); + final changeAddress = await getCurrentChangeAddress(); + + String? config; + + while (config == null) { + try { + config = Frost.createSignConfig( + network: network, + inputs: inputs, + outputs: txData.recipients!, + changeAddress: changeAddress!.value, + feePerWeight: feePerWeight, + ); + } on FrostdartException catch (e) { + if (e.errorCode == NOT_ENOUGH_FUNDS_ERROR && + utxosRemaining.isNotEmpty) { + // add extra utxo + final utxo = utxosRemaining.take(1).first; + final dData = await getDerivationData( + utxo.address, + ); + final publicKey = cryptoCurrency.addressToPubkey( + address: utxo.address!, + ); + inputs.add( + ( + utxo: utxo, scriptPubKey: publicKey, + addressDerivationData: dData, ), - ) - .toList(), - outputs: txData.recipients!, - changeAddress: (await getCurrentReceivingAddress())!.value, - feePerWeight: feePerWeight, - ); + ); + } else { + rethrow; + } + } + } return txData.copyWith(frostMSConfig: config, utxos: utxosToUse); } catch (_) { @@ -197,6 +229,44 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { } } + Future<({int account, int index, bool change})> getDerivationData( + String? address, + ) async { + if (address == null) { + throw Exception("Missing address required for FROST signing"); + } + + final addr = await mainDB.getAddress(walletId, address); + if (addr == null) { + throw Exception("Missing address in DB required for FROST signing"); + } + + final dPath = addr.derivationPath?.value ?? "0/0/0"; + + try { + final components = dPath.split("/").map((e) => int.parse(e)).toList(); + + if (components.length != 3) { + throw Exception( + "Unexpected derivation data `$components` for FROST signing", + ); + } + if (components[1] != 0 && components[1] != 1) { + throw Exception( + "${components[1]} must be 1 or 0 for change", + ); + } + + return ( + account: components[0], + change: components[1] == 1, + index: components[2], + ); + } catch (_) { + rethrow; + } + } + Future< ({ Pointer<TransactionSignMachineWrapper> machinePtr, @@ -324,16 +394,28 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { @override Future<void> updateTransactions() async { - final myAddress = (await getCurrentReceivingAddress())!; + // Get all addresses. + final List<Address> allAddressesOld = + await _fetchAddressesForElectrumXScan(); - final scriptHash = cryptoCurrency.pubKeyToScriptHash( - pubKey: Uint8List.fromList(myAddress.publicKey), - ); - final allTxHashes = - (await electrumXClient.getHistory(scripthash: scriptHash)).toSet(); + // Separate receiving and change addresses. + final Set<String> receivingAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set<String> changeAddresses = allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); + + // Remove duplicates. + final allAddressesSet = {...receivingAddresses, ...changeAddresses}; final currentHeight = await chainHeight; - final coin = info.coin; + + // Fetch history from ElectrumX. + final List<Map<String, dynamic>> allTxHashes = + await _fetchHistory(allAddressesSet); final List<Map<String, dynamic>> allTransactions = []; @@ -350,7 +432,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { final tx = await electrumXCachedClient.getTransaction( txHash: txHash["tx_hash"] as String, verbose: true, - cryptoCurrency: coin, + cryptoCurrency: cryptoCurrency, ); if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) { @@ -371,6 +453,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { // Parse inputs. BigInt amountReceivedInThisWallet = BigInt.zero; + BigInt changeAmountReceivedInThisWallet = BigInt.zero; final List<InputV2> inputs = []; for (final jsonInput in txData["vin"] as List) { final map = Map<String, dynamic>.from(jsonInput as Map); @@ -421,7 +504,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { ); // Check if input was from this wallet. - if (input.addresses.contains(myAddress.value)) { + if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) { wasSentFromThisWallet = true; input = input.copyWith(walletOwns: true); } @@ -441,10 +524,18 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { ); // If output was to my wallet, add value to amount received. - if (output.addresses.contains(myAddress.value)) { + if (receivingAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { wasReceivedInThisWallet = true; amountReceivedInThisWallet += output.value; output = output.copyWith(walletOwns: true); + } else if (changeAddresses + .intersection(output.addresses.toSet()) + .isNotEmpty) { + wasReceivedInThisWallet = true; + changeAmountReceivedInThisWallet += output.value; + output = output.copyWith(walletOwns: true); } outputs.add(output); @@ -478,7 +569,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { type = TransactionType.outgoing; if (wasReceivedInThisWallet) { - if (amountReceivedInThisWallet == totalOut) { + if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet == + totalOut) { // Definitely sent all to self. type = TransactionType.sentToSelf; } else if (amountReceivedInThisWallet == BigInt.zero) { @@ -488,6 +580,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { } else if (wasReceivedInThisWallet) { // Only found outputs owned by this wallet. type = TransactionType.incoming; + + // TODO: [prio=none] Check for special Bitcoin outputs like ordinals. } else { Logging.instance.log( "Unexpected tx found (ignoring it): $txData", @@ -524,25 +618,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { if (address == null) { final serializedKeys = await getSerializedKeys(); if (serializedKeys != null) { - final keys = frost.deserializeKeys(keys: serializedKeys); - - final addressString = frost.addressForKeys( - network: cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet, - keys: keys, - ); - - final publicKey = frost.scriptPubKeyForKeys(keys: keys); - - final address = Address( - walletId: walletId, - value: addressString, - publicKey: publicKey.toUint8ListFromHex, - derivationIndex: 0, - derivationPath: null, - subType: AddressSubType.receiving, - type: AddressType.frostMS, + final address = await _generateAddress( + change: 0, + index: 0, + serializedKeys: serializedKeys, ); await mainDB.updateOrPutAddresses([address]); @@ -729,30 +808,79 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { await mainDB.deleteWalletBlockchainData(walletId); } - final keys = frost.deserializeKeys(keys: serializedKeys!); await _saveSerializedKeys(serializedKeys!); await _saveMultisigConfig(multisigConfig!); - final addressString = frost.addressForKeys( - network: cryptoCurrency.network == CryptoCurrencyNetwork.main - ? Network.Mainnet - : Network.Testnet, - keys: keys, + const receiveChain = 0; + const changeChain = 1; + final List<Future<({int index, List<Address> addresses})>> + receiveFutures = [ + _checkGapsLinearly( + serializedKeys, + receiveChain, + ), + ]; + final List<Future<({int index, List<Address> addresses})>> + changeFutures = [ + _checkGapsLinearly( + serializedKeys, + changeChain, + ), + ]; + + // io limitations may require running these linearly instead + final futuresResult = await Future.wait([ + Future.wait(receiveFutures), + Future.wait(changeFutures), + ]); + + final receiveResults = futuresResult[0]; + final changeResults = futuresResult[1]; + + final List<Address> addressesToStore = []; + + int highestReceivingIndexWithHistory = 0; + + for (final tuple in receiveResults) { + if (tuple.addresses.isEmpty) { + await checkReceivingAddressForTransactions(); + } else { + highestReceivingIndexWithHistory = max( + tuple.index, + highestReceivingIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + int highestChangeIndexWithHistory = 0; + // If restoring a wallet that never sent any funds with change, then set changeArray + // manually. If we didn't do this, it'd store an empty array. + for (final tuple in changeResults) { + if (tuple.addresses.isEmpty) { + await checkChangeAddressForTransactions(); + } else { + highestChangeIndexWithHistory = max( + tuple.index, + highestChangeIndexWithHistory, + ); + addressesToStore.addAll(tuple.addresses); + } + } + + // remove extra addresses to help minimize risk of creating a large gap + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.change && + e.derivationIndex > highestChangeIndexWithHistory, + ); + addressesToStore.removeWhere( + (e) => + e.subType == AddressSubType.receiving && + e.derivationIndex > highestReceivingIndexWithHistory, ); - final publicKey = frost.scriptPubKeyForKeys(keys: keys); - - final address = Address( - walletId: walletId, - value: addressString, - publicKey: publicKey.toUint8ListFromHex, - derivationIndex: 0, - derivationPath: null, - subType: AddressSubType.receiving, - type: AddressType.frostMS, - ); - - await mainDB.updateOrPutAddresses([address]); + await mainDB.updateOrPutAddresses(addressesToStore); }); GlobalEventBus.instance.fire( @@ -868,23 +996,31 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { @override Future<bool> updateUTXOs() async { - final address = await getCurrentReceivingAddress(); + final allAddresses = await _fetchAddressesForElectrumXScan(); try { - final scriptHash = cryptoCurrency.pubKeyToScriptHash( - pubKey: Uint8List.fromList(address!.publicKey), - ); + final fetchedUtxoList = <List<Map<String, dynamic>>>[]; + for (int i = 0; i < allAddresses.length; i++) { + final scriptHash = cryptoCurrency.addressToScriptHash( + address: allAddresses[i].value, + ); - final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash); + final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash); + if (utxos.isNotEmpty) { + fetchedUtxoList.add(utxos); + } + } final List<UTXO> outputArray = []; - for (int i = 0; i < utxos.length; i++) { - final utxo = await _parseUTXO( - jsonUTXO: utxos[i], - ); + for (int i = 0; i < fetchedUtxoList.length; i++) { + for (int j = 0; j < fetchedUtxoList[i].length; j++) { + final utxo = await _parseUTXO( + jsonUTXO: fetchedUtxoList[i][j], + ); - outputArray.add(utxo); + outputArray.add(utxo); + } } return await mainDB.updateUTXOs(walletId, outputArray); @@ -1174,4 +1310,389 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> { return utxo; } + + @override + Future<void> checkChangeAddressForTransactions() async { + try { + final currentChange = await getCurrentChangeAddress(); + + final bool needsGenerate; + if (currentChange == null) { + // no addresses in db yet for some reason. + // Should not happen at this point... + + needsGenerate = true; + } else { + final txCount = await _fetchTxCount(address: currentChange); + needsGenerate = txCount > 0 || currentChange.derivationIndex < 0; + } + + if (needsGenerate) { + await generateNewChangeAddress(); + + // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) + // keep checking until address with no tx history is set as current + await checkChangeAddressForTransactions(); + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkChangeAddressForTransactions" + "($cryptoCurrency): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future<void> checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.log( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + level: LogLevel.Error, + ); + } + } + + try { + final currentReceiving = await getCurrentReceivingAddress(); + + final bool needsGenerate; + if (currentReceiving == null) { + // no addresses in db yet for some reason. + // Should not happen at this point... + + needsGenerate = true; + } else { + final txCount = await _fetchTxCount(address: currentReceiving); + needsGenerate = txCount > 0 || currentReceiving.derivationIndex < 0; + } + + if (needsGenerate) { + await generateNewReceivingAddress(); + + // TODO: [prio=low] Make sure we scan all addresses but only show one. + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } + } + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from _checkReceivingAddressForTransactions" + "($cryptoCurrency): $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } + + @override + Future<void> generateNewChangeAddress() async { + final current = await getCurrentChangeAddress(); + int index = current == null ? 0 : current.derivationIndex + 1; + const chain = 1; // change address + + final serializedKeys = (await getSerializedKeys())!; + + Address? address; + while (address == null) { + try { + address = await _generateAddress( + change: chain, + index: index, + serializedKeys: serializedKeys, + ); + } on FrostdartException catch (e) { + if (e.errorCode == 72) { + // rust doesn't like the addressDerivationData + index++; + continue; + } else { + rethrow; + } + } + } + + await mainDB.updateOrPutAddresses([address]); + } + + @override + Future<void> generateNewReceivingAddress() async { + final current = await getCurrentReceivingAddress(); + int index = current == null ? 0 : current.derivationIndex + 1; + const chain = 0; // receiving address + + final serializedKeys = (await getSerializedKeys())!; + + Address? address; + while (address == null) { + try { + address = await _generateAddress( + change: chain, + index: index, + serializedKeys: serializedKeys, + ); + } on FrostdartException catch (e) { + if (e.errorCode == 72) { + // rust doesn't like the addressDerivationData + index++; + continue; + } else { + rethrow; + } + } + } + + await mainDB.updateOrPutAddresses([address]); + await info.updateReceivingAddress( + newAddress: address.value, + isar: mainDB.isar, + ); + } + + Future<void> lookAhead() async { + Address? currentReceiving = await getCurrentReceivingAddress(); + if (currentReceiving == null) { + await generateNewReceivingAddress(); + currentReceiving = await getCurrentReceivingAddress(); + } + Address? currentChange = await getCurrentChangeAddress(); + if (currentChange == null) { + await generateNewChangeAddress(); + currentChange = await getCurrentChangeAddress(); + } + + final List<Address> nextReceivingAddresses = []; + final List<Address> nextChangeAddresses = []; + + int receiveIndex = currentReceiving!.derivationIndex; + int changeIndex = currentChange!.derivationIndex; + for (int i = 0; i < 10; i++) { + final receiveAddress = await _generateAddressSafe( + chain: 0, + startingIndex: receiveIndex + 1, + ); + receiveIndex = receiveAddress.derivationIndex; + nextReceivingAddresses.add(receiveAddress); + + final changeAddress = await _generateAddressSafe( + chain: 1, + startingIndex: changeIndex + 1, + ); + changeIndex = changeAddress.derivationIndex; + nextChangeAddresses.add(changeAddress); + } + + int activeReceiveIndex = currentReceiving.derivationIndex; + int activeChangeIndex = currentChange.derivationIndex; + for (final address in nextReceivingAddresses) { + final txCount = await _fetchTxCount(address: address); + if (txCount > 0) { + activeReceiveIndex = max(activeReceiveIndex, address.derivationIndex); + } + } + for (final address in nextChangeAddresses) { + final txCount = await _fetchTxCount(address: address); + if (txCount > 0) { + activeChangeIndex = max(activeChangeIndex, address.derivationIndex); + } + } + + nextReceivingAddresses + .removeWhere((e) => e.derivationIndex > activeReceiveIndex); + if (nextReceivingAddresses.isNotEmpty) { + await mainDB.updateOrPutAddresses(nextReceivingAddresses); + await info.updateReceivingAddress( + newAddress: nextReceivingAddresses.last.value, + isar: mainDB.isar, + ); + } + nextChangeAddresses + .removeWhere((e) => e.derivationIndex > activeChangeIndex); + if (nextChangeAddresses.isNotEmpty) { + await mainDB.updateOrPutAddresses(nextChangeAddresses); + } + } + + Future<Address> _generateAddressSafe({ + required final int chain, + required int startingIndex, + }) async { + final serializedKeys = (await getSerializedKeys())!; + + Address? address; + while (address == null) { + try { + address = await _generateAddress( + change: chain, + index: startingIndex, + serializedKeys: serializedKeys, + ); + } on FrostdartException catch (e) { + if (e.errorCode == 72) { + // rust doesn't like the addressDerivationData + startingIndex++; + continue; + } else { + rethrow; + } + } + } + + return address; + } + + /// Can and will often throw unless [index], [change], and [account] are zero. + /// Caller MUST handle exception! + Future<Address> _generateAddress({ + int account = 0, + required int change, + required int index, + required String serializedKeys, + }) async { + final addressDerivationData = ( + account: account, + change: change == 1, + index: index, + ); + + final keys = frost.deserializeKeys(keys: serializedKeys); + + final addressString = frost.addressForKeys( + network: cryptoCurrency.network == CryptoCurrencyNetwork.main + ? Network.Mainnet + : Network.Testnet, + keys: keys, + addressDerivationData: addressDerivationData, + ); + + return Address( + walletId: info.walletId, + value: addressString, + publicKey: cryptoCurrency.addressToPubkey(address: addressString), + derivationIndex: index, + derivationPath: DerivationPath()..value = "$account/$change/$index", + subType: change == 0 + ? AddressSubType.receiving + : change == 1 + ? AddressSubType.change + : AddressSubType.unknown, + type: AddressType.frostMS, + ); + } + + Future<({List<Address> addresses, int index})> _checkGapsLinearly( + String serializedKeys, + int chain, + ) async { + final List<Address> addressArray = []; + int gapCounter = 0; + int index = 0; + for (; gapCounter < 20; index++) { + Logging.instance.log( + "Frost index: $index, \t GapCounter chain=$chain: $gapCounter", + level: LogLevel.Info, + ); + + Address? address; + while (address == null) { + try { + address = await _generateAddress( + change: chain, + index: index, + serializedKeys: serializedKeys, + ); + } on FrostdartException catch (e) { + if (e.errorCode == 72) { + // rust doesn't like the addressDerivationData + index++; + continue; + } else { + rethrow; + } + } + } + + // get address tx count + final count = await _fetchTxCount( + address: address!, + ); + + // check and add appropriate addresses + if (count > 0) { + // add address to array + addressArray.add(address!); + // reset counter + gapCounter = 0; + // add info to derivations + } else { + // increase counter when no tx history found + gapCounter++; + } + } + + return (addresses: addressArray, index: index); + } + + Future<int> _fetchTxCount({required Address address}) async { + final transactions = await electrumXClient.getHistory( + scripthash: cryptoCurrency.addressToScriptHash( + address: address.value, + ), + ); + return transactions.length; + } + + Future<List<Address>> _fetchAddressesForElectrumXScan() async { + final allAddresses = await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); + return allAddresses; + } + + Future<List<Map<String, dynamic>>> _fetchHistory( + Iterable<String> allAddresses, + ) async { + try { + final List<Map<String, dynamic>> allTxHashes = []; + for (int i = 0; i < allAddresses.length; i++) { + final addressString = allAddresses.elementAt(i); + final scriptHash = cryptoCurrency.addressToScriptHash( + address: addressString, + ); + + final response = await electrumXClient.getHistory( + scripthash: scriptHash, + ); + + for (int j = 0; j < response.length; j++) { + response[j]["address"] = addressString; + if (!allTxHashes.contains(response[j])) { + allTxHashes.add(response[j]); + } + } + } + + return allTxHashes; + } catch (e, s) { + Logging.instance.log( + "$runtimeType._fetchHistory: $e\n$s", + level: LogLevel.Error, + ); + rethrow; + } + } } diff --git a/lib/wallets/wallet/impl/epiccash_wallet.dart b/lib/wallets/wallet/impl/epiccash_wallet.dart index 9df6a936a..2b289e84a 100644 --- a/lib/wallets/wallet/impl/epiccash_wallet.dart +++ b/lib/wallets/wallet/impl/epiccash_wallet.dart @@ -609,7 +609,7 @@ class EpiccashWallet extends Bip39Wallet { wallet: wallet!, selectionStrategyIsAll: 0, minimumConfirmations: cryptoCurrency.minConfirms, - message: txData.noteOnChain!, + message: txData.noteOnChain ?? "", amount: txData.recipients!.first.amount.raw.toInt(), address: txData.recipients!.first.address, ); diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 33ae3fb24..f9fcb11b5 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -787,7 +787,9 @@ class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T> for (final tuple in receiveResults) { if (tuple.addresses.isEmpty) { - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } } else { highestReceivingIndexWithHistory = max( tuple.index, diff --git a/lib/wallets/wallet/impl/monero_wallet.dart b/lib/wallets/wallet/impl/monero_wallet.dart index 9bcb4c3d7..9c4848b8c 100644 --- a/lib/wallets/wallet/impl/monero_wallet.dart +++ b/lib/wallets/wallet/impl/monero_wallet.dart @@ -25,6 +25,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/keys/cw_key_data.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; @@ -235,6 +236,25 @@ class MoneroWallet extends CryptonoteWallet with CwBasedInterface { return; } + @override + Future<CWKeyData?> getKeys() async { + final base = (CwBasedInterface.cwWalletBase as MoneroWalletBase?); + + if (base == null || + base.walletInfo.name != walletId || + CwBasedInterface.exitMutex.isLocked) { + return null; + } + + return CWKeyData( + walletId: walletId, + publicViewKey: base.keys.publicViewKey, + privateViewKey: base.keys.privateViewKey, + publicSpendKey: base.keys.publicSpendKey, + privateSpendKey: base.keys.privateSpendKey, + ); + } + @override Future<void> updateTransactions() async { final base = (CwBasedInterface.cwWalletBase as MoneroWalletBase?); diff --git a/lib/wallets/wallet/impl/wownero_wallet.dart b/lib/wallets/wallet/impl/wownero_wallet.dart index 69f42a57d..a3ef7d04b 100644 --- a/lib/wallets/wallet/impl/wownero_wallet.dart +++ b/lib/wallets/wallet/impl/wownero_wallet.dart @@ -28,6 +28,7 @@ import 'package:tuple/tuple.dart'; import '../../../db/hive/db.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; import '../../../models/isar/models/blockchain_data/transaction.dart'; +import '../../../models/keys/cw_key_data.dart'; import '../../../services/event_bus/events/global/tor_connection_status_changed_event.dart'; import '../../../services/event_bus/events/global/tor_status_changed_event.dart'; import '../../../services/event_bus/global_event_bus.dart'; @@ -214,6 +215,25 @@ class WowneroWallet extends CryptonoteWallet with CwBasedInterface { return; } + @override + Future<CWKeyData?> getKeys() async { + final base = (CwBasedInterface.cwWalletBase as WowneroWalletBase?); + + if (base == null || + base.walletInfo.name != walletId || + CwBasedInterface.exitMutex.isLocked) { + return null; + } + + return CWKeyData( + walletId: walletId, + publicViewKey: base.keys.publicViewKey, + privateViewKey: base.keys.privateViewKey, + publicSpendKey: base.keys.publicSpendKey, + privateSpendKey: base.keys.privateSpendKey, + ); + } + @override Future<void> updateTransactions() async { final base = (CwBasedInterface.cwWalletBase as WowneroWalletBase?); diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 1bca3c858..4837eb8d3 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -518,12 +518,18 @@ abstract class Wallet<T extends CryptoCurrency> { GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); await updateChainHeight(); + if (this is BitcoinFrostWallet) { + await (this as BitcoinFrostWallet).lookAhead(); + } + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (this is MultiAddressInterface) { - await (this as MultiAddressInterface) - .checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await (this as MultiAddressInterface) + .checkReceivingAddressForTransactions(); + } } GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart index 069571ead..661b10bd3 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/cw_based_interface.dart @@ -14,6 +14,7 @@ import 'package:mutex/mutex.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/keys/cw_key_data.dart'; import '../../../models/paymint/fee_object_model.dart'; import '../../../services/event_bus/events/global/blocks_remaining_event.dart'; import '../../../services/event_bus/events/global/refresh_percent_changed_event.dart'; @@ -24,6 +25,7 @@ import '../../../utilities/amount/amount.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/stack_file_system.dart'; import '../../crypto_currency/intermediate/cryptonote_currency.dart'; +import '../../isar/models/wallet_info.dart'; import '../intermediate/cryptonote_wallet.dart'; import 'multi_address_interface.dart'; @@ -195,6 +197,8 @@ mixin CwBasedInterface<T extends CryptonoteCurrency> on CryptonoteWallet<T> Address addressFor({required int index, int account = 0}); + Future<CWKeyData?> getKeys(); + // ============ Private ====================================================== Future<void> _refreshTxDataHelper() async { if (_txRefreshLock) return; @@ -283,7 +287,9 @@ mixin CwBasedInterface<T extends CryptonoteCurrency> on CryptonoteWallet<T> await updateTransactions(); await updateBalance(); - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + await checkReceivingAddressForTransactions(); + } if (cwWalletBase?.syncStatus is SyncedSyncStatus) { refreshMutex.release(); @@ -339,6 +345,17 @@ mixin CwBasedInterface<T extends CryptonoteCurrency> on CryptonoteWallet<T> @override Future<void> checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.log( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + level: LogLevel.Error, + ); + } + } + try { int highestIndex = -1; final entries = cwWalletBase?.transactionHistory?.transactions?.entries; @@ -377,8 +394,10 @@ mixin CwBasedInterface<T extends CryptonoteCurrency> on CryptonoteWallet<T> // we need to update the address await mainDB.updateAddress(existing, newReceivingAddress); } - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } } } on SocketException catch (se, s) { Logging.instance.log( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 035ae649f..c31f7cd4a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -23,6 +23,7 @@ import '../../../utilities/logger.dart'; import '../../../utilities/paynym_is_api.dart'; import '../../crypto_currency/coins/firo.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 '../impl/firo_wallet.dart'; @@ -1315,6 +1316,17 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> @override Future<void> checkReceivingAddressForTransactions() async { + if (info.otherData[WalletInfoKeys.reuseAddress] == true) { + try { + throw Exception(); + } catch (_, s) { + Logging.instance.log( + "checkReceivingAddressForTransactions called but reuse address flag set: $s", + level: LogLevel.Error, + ); + } + } + try { final currentReceiving = await getCurrentReceivingAddress(); @@ -1336,9 +1348,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> if (needsGenerate) { await generateNewReceivingAddress(); - // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) - // keep checking until address with no tx history is set as current - await checkReceivingAddressForTransactions(); + // TODO: [prio=low] Make sure we scan all addresses but only show one. + if (info.otherData[WalletInfoKeys.reuseAddress] != true) { + // TODO: get rid of this? Could cause problems (long loading/infinite loop or something) + // keep checking until address with no tx history is set as current + await checkReceivingAddressForTransactions(); + } } } catch (e, s) { Logging.instance.log( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart index 2e2836964..1606b1295 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart @@ -1,3 +1,4 @@ +import '../../../models/keys/xpriv_data.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; import 'electrumx_interface.dart'; @@ -42,7 +43,7 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface> ); } - Future<({List<XPriv> xprivs, String fingerprint})> getXPrivs() async { + Future<XPrivData> getXPrivs() async { final paths = cryptoCurrency.supportedDerivationPathTypes.map( (e) => ( path: e, @@ -71,7 +72,8 @@ mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface> ); }); - return ( + return XPrivData( + walletId: walletId, fingerprint: fingerprint, xprivs: [ ( diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index 09d4dc6f1..30b753be2 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -183,14 +183,75 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface> } Future<Amount> estimateFeeForSpark(Amount amount) async { - // int spendAmount = amount.raw.toInt(); - // if (spendAmount == 0) { - return Amount( - rawValue: BigInt.from(0), - fractionDigits: cryptoCurrency.fractionDigits, - ); - // } - // TODO actual fee estimation + final spendAmount = amount.raw.toInt(); + if (spendAmount == 0) { + return Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } else { + // fetch spendable spark coins + final coins = await mainDB.isar.sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .filter() + .isUsedEqualTo(false) + .and() + .heightIsNotNull() + .and() + .not() + .valueIntStringEqualTo("0") + .findAll(); + + final available = + coins.map((e) => e.value).fold(BigInt.zero, (p, e) => p + e); + + if (amount.raw > available) { + return Amount( + rawValue: BigInt.from(0), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } + + // prepare coin data for ffi + final serializedCoins = coins + .map( + (e) => ( + serializedCoin: e.serializedCoinB64!, + serializedCoinContext: e.contextB64!, + groupId: e.groupId, + height: e.height!, + ), + ) + .toList(); + + final root = await getRootHDNode(); + final String derivationPath; + if (cryptoCurrency.network.isTestNet) { + derivationPath = "$kSparkBaseDerivationPathTestnet$kDefaultSparkIndex"; + } else { + derivationPath = "$kSparkBaseDerivationPath$kDefaultSparkIndex"; + } + final privateKey = root.derivePath(derivationPath).privateKey.data; + int estimate = await _asyncSparkFeesWrapper( + privateKeyHex: privateKey.toHex, + index: kDefaultSparkIndex, + sendAmount: spendAmount, + subtractFeeFromAmount: true, + serializedCoins: serializedCoins, + // privateRecipientsCount: (txData.sparkRecipients?.length ?? 0), + privateRecipientsCount: 1, // ROUGHLY! + ); + + if (estimate < 0) { + estimate = 0; + } + + return Amount( + rawValue: BigInt.from(estimate), + fractionDigits: cryptoCurrency.fractionDigits, + ); + } } /// Spark to Spark/Transparent (spend) creation @@ -374,7 +435,7 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface> recipientCount + (txData.sparkRecipients?.length ?? 0); final BigInt estimatedFee; if (isSendAll) { - final estFee = LibSpark.estimateSparkFee( + final estFee = await _asyncSparkFeesWrapper( privateKeyHex: privateKey.toHex, index: kDefaultSparkIndex, sendAmount: txAmount.raw.toInt(), @@ -2002,3 +2063,53 @@ class MutableSparkRecipient { return 'MutableSparkRecipient{ address: $address, value: $value, memo: $memo }'; } } + +typedef SerializedCoinData = ({ + int groupId, + int height, + String serializedCoin, + String serializedCoinContext +}); + +Future<int> _asyncSparkFeesWrapper({ + required String privateKeyHex, + int index = 1, + required int sendAmount, + required bool subtractFeeFromAmount, + required List<SerializedCoinData> serializedCoins, + required int privateRecipientsCount, +}) async { + return await compute( + _estSparkFeeComputeFunc, + ( + privateKeyHex: privateKeyHex, + index: index, + sendAmount: sendAmount, + subtractFeeFromAmount: subtractFeeFromAmount, + serializedCoins: serializedCoins, + privateRecipientsCount: privateRecipientsCount, + ), + ); +} + +int _estSparkFeeComputeFunc( + ({ + String privateKeyHex, + int index, + int sendAmount, + bool subtractFeeFromAmount, + List<SerializedCoinData> serializedCoins, + int privateRecipientsCount, + }) args, +) { + final est = LibSpark.estimateSparkFee( + privateKeyHex: args.privateKeyHex, + index: args.index, + sendAmount: args.sendAmount, + subtractFeeFromAmount: args.subtractFeeFromAmount, + serializedCoins: args.serializedCoins, + privateRecipientsCount: args.privateRecipientsCount, + ); + + return est; +} diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index b83b09696..cab5043d6 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -181,8 +181,7 @@ class _TransactionCardState extends ConsumerState<TransactionCard> { unawaited( showFloatingFlushBar( context: context, - message: - "Restored Epic funds from your Seed have no Data.\nUse Stack Backup to keep your transaction history.", + message: "Restored Epic funds from your Seed have no Data.", type: FlushBarType.warning, duration: const Duration(seconds: 5), ), diff --git a/pubspec.lock b/pubspec.lock index 7a25ed30b..f311f27ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -287,21 +287,23 @@ packages: source: hosted version: "4.6.0" coinlib: - dependency: transitive + dependency: "direct overridden" description: - name: coinlib - sha256: "44aa3f7b07d3b03d58353e7657f43cdaf76a70ad2cce5bdac9306208099d8df5" - url: "https://pub.dev" - source: hosted - version: "2.0.0" + path: coinlib + ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + resolved-ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + url: "https://github.com/peercoin/coinlib.git" + source: git + version: "2.1.0-rc.1" coinlib_flutter: dependency: "direct main" description: - name: coinlib_flutter - sha256: b352378773158dbaec37bd542c297682f3812f9881acb676971f0f4c5893631f - url: "https://pub.dev" - source: hosted - version: "2.0.0" + path: coinlib_flutter + ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + resolved-ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + url: "https://github.com/peercoin/coinlib.git" + source: git + version: "2.1.0-rc.1" collection: dependency: transitive description: @@ -1062,26 +1064,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lelantus: dependency: "direct main" description: @@ -1149,10 +1151,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1588,8 +1590,8 @@ packages: dependency: "direct main" description: path: "packages/solana" - ref: a83e375678eb22fe544dc125d29bbec0fb833882 - resolved-ref: a83e375678eb22fe544dc125d29bbec0fb833882 + ref: "706be5f166d31736c443cca4cd311b7fcfc2a9bf" + resolved-ref: "706be5f166d31736c443cca4cd311b7fcfc2a9bf" url: "https://github.com/cypherstack/espresso-cash-public.git" source: git version: "0.30.4" @@ -1734,26 +1736,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" tezart: dependency: "direct main" description: @@ -1952,10 +1954,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" wakelock: dependency: "direct main" description: diff --git a/scripts/app_config/configure_campfire.sh b/scripts/app_config/configure_campfire.sh index 9033255d9..ae6bba36d 100755 --- a/scripts/app_config/configure_campfire.sh +++ b/scripts/app_config/configure_campfire.sh @@ -43,6 +43,8 @@ part of 'app_config.dart'; const _prefix = "Campfire"; const _separator = ""; const _suffix = ""; +const _emptyWalletsMessage = + "Join us around the Campfire and create a wallet!"; const _appDataDirName = "campfire"; const _shortDescriptionText = "Your privacy. Your wallet. Your Firo."; const _commitHash = "$BUILT_COMMIT_HASH"; diff --git a/scripts/app_config/configure_stack_duo.sh b/scripts/app_config/configure_stack_duo.sh index 135d03958..143faf644 100755 --- a/scripts/app_config/configure_stack_duo.sh +++ b/scripts/app_config/configure_stack_duo.sh @@ -37,6 +37,8 @@ part of 'app_config.dart'; const _prefix = "Stack"; const _separator = " "; const _suffix = "Duo"; +const _emptyWalletsMessage = + "You do not have any wallets yet. Start building your crypto Stack!"; const _appDataDirName = "stackduo"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _commitHash = "$BUILT_COMMIT_HASH"; diff --git a/scripts/app_config/configure_stack_wallet.sh b/scripts/app_config/configure_stack_wallet.sh index 2c798e53f..7f7c51ab7 100755 --- a/scripts/app_config/configure_stack_wallet.sh +++ b/scripts/app_config/configure_stack_wallet.sh @@ -37,6 +37,8 @@ part of 'app_config.dart'; const _prefix = "Stack"; const _separator = " "; const _suffix = "Wallet"; +const _emptyWalletsMessage = + "You do not have any wallets yet. Start building your crypto Stack!"; const _appDataDirName = "stackwallet"; const _shortDescriptionText = "An open-source, multicoin wallet for everyone"; const _commitHash = "$BUILT_COMMIT_HASH"; diff --git a/scripts/app_config/platforms/ios/platform_config.sh b/scripts/app_config/platforms/ios/platform_config.sh index b60d3615c..c2842d1d5 100755 --- a/scripts/app_config/platforms/ios/platform_config.sh +++ b/scripts/app_config/platforms/ios/platform_config.sh @@ -15,3 +15,22 @@ done # Configure ios for Duo. sed -i '' "s/${APP_NAME_PLACEHOLDER}/${NEW_NAME}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_0}" sed -i '' "s/${APP_ID_PLACEHOLDER}/${NEW_APP_ID}/g" "${APP_PROJECT_ROOT_DIR}/${IOS_TF_1}" + +# use app specific launch images +LAUNCH_IMAGES_DIR="${APP_PROJECT_ROOT_DIR}/ios/Runner/Assets.xcassets/LaunchImage.imageset" +for file in "${LAUNCH_IMAGES_DIR}"/*.png; +do + # Check if the file exists to avoid errors if no PNG files are found + if [ -f "${file}" ]; then + rm "${file}" + fi +done + +LAUNCH_IMAGES_TEMPLATES_DIR="${APP_PROJECT_ROOT_DIR}/asset_sources/other/ios_launch_image/${NEW_BASIC_NAME}" +for file in "${LAUNCH_IMAGES_TEMPLATES_DIR}"/*.png; +do + # Check if the file exists to avoid errors if no PNG files are found + if [ -f "${file}" ]; then + cp "${file}" "${LAUNCH_IMAGES_DIR}/" + fi +done \ No newline at end of file diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index e7eb439f7..a784dbabd 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -171,7 +171,12 @@ dependencies: convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 - coinlib_flutter: ^2.0.0 +# coinlib_flutter: ^2.1.0-rc.1 + coinlib_flutter: + git: + url: https://github.com/peercoin/coinlib.git + ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + path: coinlib_flutter electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git @@ -180,7 +185,7 @@ dependencies: solana: git: # TODO [prio=low]: Revert to official package once Tor support is merged upstream. url: https://github.com/cypherstack/espresso-cash-public.git - ref: a83e375678eb22fe544dc125d29bbec0fb833882 + ref: 706be5f166d31736c443cca4cd311b7fcfc2a9bf path: packages/solana calendar_date_picker2: ^1.0.2 sqlite3: ^2.4.3 @@ -210,6 +215,20 @@ flutter_native_splash: android_disable_fullscreen: true dependency_overrides: + + # coin lib git for testing while waiting for publishing + coinlib: + git: + url: https://github.com/peercoin/coinlib.git + ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + path: coinlib + + coinlib_flutter: + git: + url: https://github.com/peercoin/coinlib.git + ref: b88e68e2e10ffe54d802deeed6b9e83da7721a1f + path: coinlib_flutter + # adding here due to pure laziness tor_ffi_plugin: git: diff --git a/scripts/prebuild.ps1 b/scripts/prebuild.ps1 index 80a6991b7..04b68bc35 100644 --- a/scripts/prebuild.ps1 +++ b/scripts/prebuild.ps1 @@ -2,7 +2,7 @@ $KEYS = "..\lib\external_api_keys.dart" if (-not (Test-Path $KEYS)) { Write-Host "prebuild.ps1: creating template lib/external_api_keys.dart file" - "const kChangeNowApiKey = '';" + "`nconst kSimpleSwapApiKey = '';" | Out-File $KEYS -Encoding UTF8 + "const kChangeNowApiKey = '';" + "`nconst kSimpleSwapApiKey = '';" + "`nconst kNanswapApiKey = '';" + "`nconst kNanoSwapRpcApiKey = '';" | Out-File $KEYS -Encoding UTF8 } # Create template wallet test parameter files if they don't already exist diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh index 77d65b253..6c50fbefd 100755 --- a/scripts/prebuild.sh +++ b/scripts/prebuild.sh @@ -4,7 +4,7 @@ KEYS=../lib/external_api_keys.dart if ! test -f "$KEYS"; then echo 'prebuild.sh: creating template lib/external_api_keys.dart file' - printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\n' > $KEYS + printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\nconst kNanswapApiKey = "";\nconst kNanoSwapRpcApiKey = "";\n' > $KEYS fi # Create template wallet test parameter files if they don't already exist diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index 8a0ea1c71..db289eae0 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -147,6 +147,15 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValueForMissingStub: _i7.Future<void>.value(), ) as _i7.Future<void>); @override + _i7.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i7.Future<void>.value(), + returnValueForMissingStub: _i7.Future<void>.value(), + ) as _i7.Future<void>); + @override _i7.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -465,7 +474,14 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i7.Future<Set<String>>.value(<String>{}), ) as _i7.Future<Set<String>>); @override - _i7.Future<Map<String, dynamic>> getMempoolSparkData({ + _i7.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -478,9 +494,27 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i7.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i7.Future<Map<String, dynamic>>); + returnValue: _i7.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i7.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i7.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -498,6 +532,24 @@ class MockElectrumXClient extends _i1.Mock implements _i5.ElectrumXClient { returnValue: _i7.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i7.Future<List<List<dynamic>>>); @override + _i7.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i7.Future<bool>.value(false), + ) as _i7.Future<bool>); + @override _i7.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( @@ -941,6 +993,19 @@ class MockPrefs extends _i1.Mock implements _i8.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 6a0bc127f..17887db35 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -1000,6 +1000,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index f00e95494..d883616bf 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -473,6 +473,19 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart index 078584849..474c6e622 100644 --- a/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart +++ b/test/services/coins/bitcoin/bitcoin_wallet_test.mocks.dart @@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValueForMissingStub: _i6.Future<void>.value(), ) as _i6.Future<void>); @override + _i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override _i6.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<Set<String>>.value(<String>{}), ) as _i6.Future<Set<String>>); @override - _i6.Future<Map<String, dynamic>> getMempoolSparkData({ + _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i6.Future<Map<String, dynamic>>); + returnValue: _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i6.Future<List<List<dynamic>>>); @override + _i6.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart index 25ae15056..00b2be75f 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.mocks.dart @@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValueForMissingStub: _i6.Future<void>.value(), ) as _i6.Future<void>); @override + _i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override _i6.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<Set<String>>.value(<String>{}), ) as _i6.Future<Set<String>>); @override - _i6.Future<Map<String, dynamic>> getMempoolSparkData({ + _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i6.Future<Map<String, dynamic>>); + returnValue: _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i6.Future<List<List<dynamic>>>); @override + _i6.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart index 24b7bd226..45316c474 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.mocks.dart @@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValueForMissingStub: _i6.Future<void>.value(), ) as _i6.Future<void>); @override + _i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override _i6.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<Set<String>>.value(<String>{}), ) as _i6.Future<Set<String>>); @override - _i6.Future<Map<String, dynamic>> getMempoolSparkData({ + _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i6.Future<Map<String, dynamic>>); + returnValue: _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i6.Future<List<List<dynamic>>>); @override + _i6.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart index e8927e3e7..163972e97 100644 --- a/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart +++ b/test/services/coins/namecoin/namecoin_wallet_test.mocks.dart @@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValueForMissingStub: _i6.Future<void>.value(), ) as _i6.Future<void>); @override + _i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override _i6.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<Set<String>>.value(<String>{}), ) as _i6.Future<Set<String>>); @override - _i6.Future<Map<String, dynamic>> getMempoolSparkData({ + _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i6.Future<Map<String, dynamic>>); + returnValue: _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i6.Future<List<List<dynamic>>>); @override + _i6.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( diff --git a/test/services/coins/particl/particl_wallet_test.mocks.dart b/test/services/coins/particl/particl_wallet_test.mocks.dart index 3490fc59f..44f0da5ad 100644 --- a/test/services/coins/particl/particl_wallet_test.mocks.dart +++ b/test/services/coins/particl/particl_wallet_test.mocks.dart @@ -144,6 +144,15 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValueForMissingStub: _i6.Future<void>.value(), ) as _i6.Future<void>); @override + _i6.Future<void> checkElectrumAdapter() => (super.noSuchMethod( + Invocation.method( + #checkElectrumAdapter, + [], + ), + returnValue: _i6.Future<void>.value(), + returnValueForMissingStub: _i6.Future<void>.value(), + ) as _i6.Future<void>); + @override _i6.Future<dynamic> request({ required String? command, List<dynamic>? args = const [], @@ -462,7 +471,14 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<Set<String>>.value(<String>{}), ) as _i6.Future<Set<String>>); @override - _i6.Future<Map<String, dynamic>> getMempoolSparkData({ + _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>> getMempoolSparkData({ String? requestID, required List<String>? txids, }) => @@ -475,9 +491,27 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { #txids: txids, }, ), - returnValue: - _i6.Future<Map<String, dynamic>>.value(<String, dynamic>{}), - ) as _i6.Future<Map<String, dynamic>>); + returnValue: _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>.value(<({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>[]), + ) as _i6.Future< + List< + ({ + List<String> coins, + List<String> lTags, + List<String> serialContext, + String txid + })>>); @override _i6.Future<List<List<dynamic>>> getSparkUnhashedUsedCoinsTagsWithTxHashes({ String? requestID, @@ -495,6 +529,24 @@ class MockElectrumXClient extends _i1.Mock implements _i4.ElectrumXClient { returnValue: _i6.Future<List<List<dynamic>>>.value(<List<dynamic>>[]), ) as _i6.Future<List<List<dynamic>>>); @override + _i6.Future<bool> isMasterNodeCollateral({ + String? requestID, + required String? txid, + required int? index, + }) => + (super.noSuchMethod( + Invocation.method( + #isMasterNodeCollateral, + [], + { + #requestID: requestID, + #txid: txid, + #index: index, + }, + ), + returnValue: _i6.Future<bool>.value(false), + ) as _i6.Future<bool>); + @override _i6.Future<Map<String, dynamic>> getFeeRate({String? requestID}) => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index dad98a259..42529c1a4 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -729,6 +729,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 59450b63e..46678d619 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -615,6 +615,19 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index cc4157a10..beca96c89 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -718,6 +718,19 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); @override + bool get autoPin => (super.noSuchMethod( + Invocation.getter(#autoPin), + returnValue: false, + ) as bool); + @override + set autoPin(bool? autoPin) => super.noSuchMethod( + Invocation.setter( + #autoPin, + autoPin, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), returnValue: false,