diff --git a/lib/electrumx_rpc/electrumx_client.dart b/lib/electrumx_rpc/electrumx_client.dart index da5cf2778..befe70fc3 100644 --- a/lib/electrumx_rpc/electrumx_client.dart +++ b/lib/electrumx_rpc/electrumx_client.dart @@ -1036,7 +1036,7 @@ class ElectrumXClient { } } - /// Returns the txids of the current transactions found in the mempool + /// Returns the txids of the current spark transactions found in the mempool Future> getMempoolTxids({String? requestID}) async { try { final start = DateTime.now(); @@ -1089,6 +1089,7 @@ class ElectrumXClient { // the space after lTags is required lol lTags: List.from(entry.value["lTags "] as List), coins: List.from(entry.value["coins"] as List), + isLocked: entry.value["isLocked"] as bool, ), ); } diff --git a/lib/models/electrumx_response/spark_models.dart b/lib/models/electrumx_response/spark_models.dart index 43bf9f61f..22c6cf25f 100644 --- a/lib/models/electrumx_response/spark_models.dart +++ b/lib/models/electrumx_response/spark_models.dart @@ -3,12 +3,14 @@ class SparkMempoolData { final List serialContext; final List lTags; final List coins; + final bool isLocked; SparkMempoolData({ required this.txid, required this.serialContext, required this.lTags, required this.coins, + required this.isLocked, }); @override @@ -17,7 +19,8 @@ class SparkMempoolData { "txid: $txid, " "serialContext: $serialContext, " "lTags: $lTags, " - "coins: $coins" + "coins: $coins, " + "isLocked: $isLocked" "}"; } } diff --git a/lib/pages/spark_names/buy_spark_name_view.dart b/lib/pages/spark_names/buy_spark_name_view.dart index 01f69fcaf..036a551d3 100644 --- a/lib/pages/spark_names/buy_spark_name_view.dart +++ b/lib/pages/spark_names/buy_spark_name_view.dart @@ -23,6 +23,7 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; +import '../../wallets/crypto_currency/coins/firo.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart'; import '../../widgets/background.dart'; @@ -59,6 +60,7 @@ class _BuySparkNameViewState extends ConsumerState { String get _title => isRenewal ? "Renew name" : "Buy name"; int _years = 1; + late bool _buttonEnabled; bool _lockAddressFill = false; Future _fillCurrentReceiving() async { @@ -80,9 +82,21 @@ class _BuySparkNameViewState extends ConsumerState { } Future _preRegFuture() async { + final chosenAddress = addressController.text; + + if (chosenAddress.isEmpty) { + throw Exception( + "Please select the Spark address you want to link to your Spark Name", + ); + } + final wallet = ref.read(pWallets).getWallet(widget.walletId) as SparkInterface; + if (!(wallet.cryptoCurrency as Firo).validateSparkAddress(chosenAddress)) { + throw Exception("Invalid Spark address selected"); + } + final myAddresses = await wallet.mainDB.isar.addresses .where() @@ -94,10 +108,8 @@ class _BuySparkNameViewState extends ConsumerState { .valueProperty() .findAll(); - final chosenAddress = addressController.text; - if (!myAddresses.contains(chosenAddress)) { - throw Exception("Address does not belong to this wallet"); + throw Exception("Selected Spark address does not belong to this wallet"); } final txData = await wallet.prepareSparkNameTransaction( @@ -174,10 +186,19 @@ class _BuySparkNameViewState extends ConsumerState { @override void initState() { super.initState(); + if (isRenewal) { additionalInfoController.text = widget.nameToRenew!.additionalInfo ?? ""; addressController.text = widget.nameToRenew!.address; } + _buttonEnabled = addressController.text.isNotEmpty; + addressController.addListener(() { + if (mounted) { + setState(() { + _buttonEnabled = addressController.text.isNotEmpty; + }); + } + }); } @override @@ -274,7 +295,7 @@ class _BuySparkNameViewState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "Address", + "Spark address", style: Util.isDesktop ? STextStyles.w500_14(context).copyWith( @@ -311,7 +332,7 @@ class _BuySparkNameViewState extends ConsumerState { isDense: true, contentPadding: const EdgeInsets.all(16), hintStyle: STextStyles.fieldLabel(context), - hintText: "Address", + hintText: "Spark address (required)", border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, @@ -360,7 +381,7 @@ class _BuySparkNameViewState extends ConsumerState { isDense: true, contentPadding: const EdgeInsets.all(16), hintStyle: STextStyles.fieldLabel(context), - hintText: "Additional info", + hintText: "Additional info (optional)", border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, @@ -516,7 +537,8 @@ class _BuySparkNameViewState extends ConsumerState { PrimaryButton( label: isRenewal ? "Renew" : "Buy", buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: _prepareNameTx, + enabled: _buttonEnabled, + onPressed: _buttonEnabled ? _prepareNameTx : null, ), SizedBox(height: Util.isDesktop ? 32 : 16), ], diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index dd4c29560..1bb76d59e 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1060,6 +1060,7 @@ class _WalletViewState extends ConsumerState { ), if (Constants.enableExchange && ref.watch(pWalletCoin(walletId)) is! FrostCurrency && + wallet is! FiroWallet && AppConfig.hasFeature(AppFeature.buy) && showExchange) WalletNavigationBarItemData( @@ -1067,6 +1068,17 @@ class _WalletViewState extends ConsumerState { icon: const BuyNavIcon(), onTap: () => _onBuyPressed(context), ), + if (wallet is SparkInterface) + WalletNavigationBarItemData( + label: "Names", + icon: const PaynymNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + SparkNamesHomeView.routeName, + arguments: widget.walletId, + ); + }, + ), ], moreItems: [ if (ref.watch( @@ -1153,17 +1165,6 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (wallet is SparkInterface) - WalletNavigationBarItemData( - label: "Names", - icon: const PaynymNavIcon(), - onTap: () { - Navigator.of(context).pushNamed( - SparkNamesHomeView.routeName, - arguments: widget.walletId, - ); - }, - ), if (!viewOnly && wallet is PaynymInterface) WalletNavigationBarItemData( label: "PayNym", 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 63722608b..71cd7f196 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 @@ -403,6 +403,10 @@ class _DesktopWalletFeaturesState extends ConsumerState { Assets.svg.recycle, _onAnonymizeAllPressed, ), + + if (wallet is SparkInterface) + (WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed), + if (!isViewOnly && Constants.enableExchange && AppConfig.hasFeature(AppFeature.swap) && @@ -412,9 +416,6 @@ class _DesktopWalletFeaturesState extends ConsumerState { if (showExchange && AppConfig.hasFeature(AppFeature.buy)) (WalletFeature.buy, Assets.svg.swap, _onBuyPressed), - if (wallet is SparkInterface) - (WalletFeature.sparkNames, Assets.svg.robotHead, _onSparkNamesPressed), - if (showCoinControl) ( WalletFeature.coinControl, @@ -463,9 +464,10 @@ class _DesktopWalletFeaturesState extends ConsumerState { final options = _getOptions( wallet, - ref.watch( - prefsChangeNotifierProvider.select((value) => value.enableExchange), - ), + wallet is! FiroWallet && + ref.watch( + prefsChangeNotifierProvider.select((value) => value.enableExchange), + ), (wallet is CoinControlInterface && ref.watch( prefsChangeNotifierProvider.select( diff --git a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart index 8a937fab3..c2f2519c6 100644 --- a/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart +++ b/lib/pages_desktop_specific/spark_coins/spark_coins_view.dart @@ -26,10 +26,7 @@ import '../../widgets/desktop/desktop_scaffold.dart'; import '../../widgets/isar_collection_watcher_list.dart'; class SparkCoinsView extends ConsumerWidget { - const SparkCoinsView({ - super.key, - required this.walletId, - }); + const SparkCoinsView({super.key, required this.walletId}); static const title = "Spark coins"; static const String routeName = "/sparkCoinsView"; @@ -47,32 +44,27 @@ class SparkCoinsView extends ConsumerWidget { leading: Expanded( child: Row( children: [ - const SizedBox( - width: 32, - ), + const SizedBox(width: 32), AppBarIconButton( size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, + color: + Theme.of( + context, + ).extension()!.textFieldDefaultBG, shadows: const [], icon: SvgPicture.asset( Assets.svg.arrowLeft, width: 18, height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, + color: + Theme.of( + context, + ).extension()!.topNavIconPrimary, ), onPressed: Navigator.of(context).pop, ), - const SizedBox( - width: 12, - ), - Text( - title, - style: STextStyles.desktopH3(context), - ), + const SizedBox(width: 12), + Text(title, style: STextStyles.desktopH3(context)), const Spacer(), ], ), @@ -80,10 +72,7 @@ class SparkCoinsView extends ConsumerWidget { useSpacers: false, isCompactHeight: true, ), - body: Padding( - padding: const EdgeInsets.all(24), - child: child, - ), + body: Padding(padding: const EdgeInsets.all(24), child: child), ); }, child: ConditionalParent( @@ -98,26 +87,23 @@ class SparkCoinsView extends ConsumerWidget { leading: AppBarBackButton( onPressed: () => Navigator.of(context).pop(), ), - title: Text( - title, - style: STextStyles.navBarTitle(context), - ), - ), - body: SafeArea( - child: child, + title: Text(title, style: STextStyles.navBarTitle(context)), ), + body: SafeArea(child: child), ), ); }, child: IsarCollectionWatcherList( itemName: title, - queryBuilder: () => ref - .read(mainDBProvider) - .isar - .sparkCoins - .where() - .walletIdEqualToAnyLTagHash(walletId) - .sortByHeightDesc(), + queryBuilder: + () => + ref + .read(mainDBProvider) + .isar + .sparkCoins + .where() + .walletIdEqualToAnyLTagHash(walletId) + .sortByHeightDesc(), itemBuilder: (SparkCoin? coin) { return [ ("TXID", coin?.txHash ?? "", 9), @@ -129,6 +115,7 @@ class SparkCoinsView extends ConsumerWidget { ("Group ID", coin?.groupId.toString() ?? "", 2), ("Type", coin?.type.name ?? "", 2), ("Used", coin?.isUsed.toString() ?? "", 2), + ("Locked", coin?.isLocked.toString() ?? "", 2), ]; }, ), diff --git a/lib/wallets/crypto_currency/coins/banano.dart b/lib/wallets/crypto_currency/coins/banano.dart index eccb6fe38..adae4331e 100644 --- a/lib/wallets/crypto_currency/coins/banano.dart +++ b/lib/wallets/crypto_currency/coins/banano.dart @@ -89,7 +89,7 @@ class Banano extends NanoCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://www.bananolooker.com/block/$txid"); + return Uri.parse("https://creeper.banano.cc/hash/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", diff --git a/lib/wallets/crypto_currency/coins/nano.dart b/lib/wallets/crypto_currency/coins/nano.dart index 04622c54d..77fc9e3a7 100644 --- a/lib/wallets/crypto_currency/coins/nano.dart +++ b/lib/wallets/crypto_currency/coins/nano.dart @@ -95,7 +95,7 @@ class Nano extends NanoCurrency { Uri defaultBlockExplorer(String txid) { switch (network) { case CryptoCurrencyNetwork.main: - return Uri.parse("https://www.nanolooker.com/block/$txid"); + return Uri.parse("https://nanexplorer.com/nano/blocks/$txid"); default: throw Exception( "Unsupported network for defaultBlockExplorer(): $network", diff --git a/lib/wallets/isar/models/spark_coin.dart b/lib/wallets/isar/models/spark_coin.dart index 9501bf06d..c16cee253 100644 --- a/lib/wallets/isar/models/spark_coin.dart +++ b/lib/wallets/isar/models/spark_coin.dart @@ -17,13 +17,7 @@ enum SparkCoinType { class SparkCoin { Id id = Isar.autoIncrement; - @Index( - unique: true, - replace: true, - composite: [ - CompositeIndex("lTagHash"), - ], - ) + @Index(unique: true, replace: true, composite: [CompositeIndex("lTagHash")]) final String walletId; @enumerated @@ -55,6 +49,10 @@ class SparkCoin { final String? serializedCoinB64; final String? contextB64; + // prefix name with zzz to ensure serialization order remains unchanged + @Name("zzzIsLocked") + final bool? isLocked; + @ignore BigInt get value => BigInt.parse(valueIntString); @@ -66,10 +64,7 @@ class SparkCoin { return max(0, currentChainHeight - (height! - 1)); } - bool isConfirmed( - int currentChainHeight, - int minimumConfirms, - ) { + bool isConfirmed(int currentChainHeight, int minimumConfirms) { final confirmations = getConfirmations(currentChainHeight); return confirmations >= minimumConfirms; } @@ -93,6 +88,7 @@ class SparkCoin { this.height, this.serializedCoinB64, this.contextB64, + this.isLocked, }); SparkCoin copyWith({ @@ -113,6 +109,7 @@ class SparkCoin { int? height, String? serializedCoinB64, String? contextB64, + bool? isLocked, }) { return SparkCoin( walletId: walletId, @@ -134,6 +131,7 @@ class SparkCoin { height: height ?? this.height, serializedCoinB64: serializedCoinB64 ?? this.serializedCoinB64, contextB64: contextB64 ?? this.contextB64, + isLocked: isLocked ?? this.isLocked, ); } @@ -158,6 +156,7 @@ class SparkCoin { ', height: $height' ', serializedCoinB64: $serializedCoinB64' ', contextB64: $contextB64' + ', isLocked: $isLocked' ')'; } } diff --git a/lib/wallets/isar/models/spark_coin.g.dart b/lib/wallets/isar/models/spark_coin.g.dart index 717d1a2c6..c84e0db40 100644 --- a/lib/wallets/isar/models/spark_coin.g.dart +++ b/lib/wallets/isar/models/spark_coin.g.dart @@ -107,6 +107,11 @@ const SparkCoinSchema = CollectionSchema( id: 17, name: r'walletId', type: IsarType.string, + ), + r'zzzIsLocked': PropertySchema( + id: 18, + name: r'zzzIsLocked', + type: IsarType.bool, ) }, estimateSize: _sparkCoinEstimateSize, @@ -229,6 +234,7 @@ void _sparkCoinSerialize( writer.writeByte(offsets[15], object.type.index); writer.writeString(offsets[16], object.valueIntString); writer.writeString(offsets[17], object.walletId); + writer.writeBool(offsets[18], object.isLocked); } SparkCoin _sparkCoinDeserialize( @@ -257,6 +263,7 @@ SparkCoin _sparkCoinDeserialize( SparkCoinType.mint, valueIntString: reader.readString(offsets[16]), walletId: reader.readString(offsets[17]), + isLocked: reader.readBoolOrNull(offsets[18]), ); object.id = id; return object; @@ -306,6 +313,8 @@ P _sparkCoinDeserializeProp

( return (reader.readString(offset)) as P; case 17: return (reader.readString(offset)) as P; + case 18: + return (reader.readBoolOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -2867,6 +2876,33 @@ extension SparkCoinQueryFilter )); }); } + + QueryBuilder isLockedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'zzzIsLocked', + )); + }); + } + + QueryBuilder + isLockedIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'zzzIsLocked', + )); + }); + } + + QueryBuilder isLockedEqualTo( + bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'zzzIsLocked', + value: value, + )); + }); + } } extension SparkCoinQueryObject @@ -3034,6 +3070,18 @@ extension SparkCoinQuerySortBy on QueryBuilder { return query.addSortBy(r'walletId', Sort.desc); }); } + + QueryBuilder sortByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.asc); + }); + } + + QueryBuilder sortByIsLockedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.desc); + }); + } } extension SparkCoinQuerySortThenBy @@ -3208,6 +3256,18 @@ extension SparkCoinQuerySortThenBy return query.addSortBy(r'walletId', Sort.desc); }); } + + QueryBuilder thenByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.asc); + }); + } + + QueryBuilder thenByIsLockedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'zzzIsLocked', Sort.desc); + }); + } } extension SparkCoinQueryWhereDistinct @@ -3332,6 +3392,12 @@ extension SparkCoinQueryWhereDistinct return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); }); } + + QueryBuilder distinctByIsLocked() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'zzzIsLocked'); + }); + } } extension SparkCoinQueryProperty @@ -3453,4 +3519,10 @@ extension SparkCoinQueryProperty return query.addPropertyName(r'walletId'); }); } + + QueryBuilder isLockedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'zzzIsLocked'); + }); + } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart index d5d5e756d..af6894676 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart @@ -880,7 +880,12 @@ mixin SparkInterface // add checked txids after identification _mempoolTxidsChecked.addAll(checkedTxids); - result.addAll(myCoins); + for (final coin in myCoins) { + final match = sparkDataToCheck.firstWhere( + (e) => e.serialContext.contains(coin.contextB64!), + ); + result.add(coin.copyWith(isLocked: match.isLocked)); + } } return result;