From c274ecaf634a39bc380348d1baa31067dfa42ee7 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 12 Feb 2025 19:49:36 -0600 Subject: [PATCH 01/29] build runner update mocks --- test/cached_electrumx_test.mocks.dart | 56 ++- .../pages/send_view/send_view_test.mocks.dart | 50 ++- .../exchange/exchange_view_test.mocks.dart | 383 ++++++++++-------- .../managed_favorite_test.mocks.dart | 108 +++-- .../node_options_sheet_test.mocks.dart | 116 ++++-- .../transaction_card_test.mocks.dart | 280 +++++++------ 6 files changed, 617 insertions(+), 376 deletions(-) diff --git a/test/cached_electrumx_test.mocks.dart b/test/cached_electrumx_test.mocks.dart index a5c7c4c54..4ac0b9849 100644 --- a/test/cached_electrumx_test.mocks.dart +++ b/test/cached_electrumx_test.mocks.dart @@ -4,14 +4,15 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; -import 'dart:ui' as _i14; +import 'dart:ui' as _i15; import 'package:decimal/decimal.dart' as _i4; +import 'package:logger/logger.dart' as _i13; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i8; import 'package:stackwallet/electrumx_rpc/electrumx_client.dart' as _i6; import 'package:stackwallet/models/electrumx_response/spark_models.dart' as _i3; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i13; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i14; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i12; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i11; import 'package:stackwallet/utilities/prefs.dart' as _i10; @@ -1130,6 +1131,45 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i13.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i13.Level.all, + ) as _i13.Level); + + @override + set logLevel(_i13.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1186,18 +1226,18 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ) as _i9.Future); @override - _i13.AmountUnit amountUnit(_i2.CryptoCurrency? coin) => (super.noSuchMethod( + _i14.AmountUnit amountUnit(_i2.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i13.AmountUnit.normal, - ) as _i13.AmountUnit); + returnValue: _i14.AmountUnit.normal, + ) as _i14.AmountUnit); @override void updateAmountUnit({ required _i2.CryptoCurrency? coin, - required _i13.AmountUnit? amountUnit, + required _i14.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -1270,7 +1310,7 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override - void addListener(_i14.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i15.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1279,7 +1319,7 @@ class MockPrefs extends _i1.Mock implements _i10.Prefs { ); @override - void removeListener(_i14.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i15.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index fee6a0d7f..476ab3883 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -7,6 +7,7 @@ import 'dart:async' as _i10; import 'dart:typed_data' as _i19; import 'dart:ui' as _i14; +import 'package:logger/logger.dart' as _i22; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:stackwallet/db/isar/main_db.dart' as _i3; @@ -17,7 +18,7 @@ import 'package:stackwallet/services/locale_service.dart' as _i15; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i9; import 'package:stackwallet/themes/theme_service.dart' as _i17; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i22; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i23; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i21; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i20; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -1128,6 +1129,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i22.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i22.Level.all, + ) as _i22.Level); + + @override + set logLevel(_i22.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -1184,18 +1224,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i22.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i23.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i22.AmountUnit.normal, - ) as _i22.AmountUnit); + returnValue: _i23.AmountUnit.normal, + ) as _i23.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i22.AmountUnit? amountUnit, + required _i23.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index b7a8d49d5..565f05f82 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -3,40 +3,41 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:ui' as _i12; +import 'dart:async' as _i10; +import 'dart:ui' as _i13; -import 'package:decimal/decimal.dart' as _i18; +import 'package:decimal/decimal.dart' as _i19; +import 'package:logger/logger.dart' as _i9; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i7; import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart' - as _i21; + as _i22; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart' - as _i23; -import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' as _i24; +import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart' + as _i25; import 'package:stackwallet/models/exchange/response_objects/estimate.dart' - as _i20; + as _i21; import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart' - as _i22; + as _i23; import 'package:stackwallet/models/exchange/response_objects/range.dart' - as _i19; + as _i20; import 'package:stackwallet/models/exchange/response_objects/trade.dart' - as _i14; -import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i17; -import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i25; + as _i15; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart' as _i18; +import 'package:stackwallet/models/isar/exchange_cache/pair.dart' as _i26; import 'package:stackwallet/networking/http.dart' as _i3; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart' - as _i16; + as _i17; import 'package:stackwallet/services/exchange/exchange_response.dart' as _i4; -import 'package:stackwallet/services/trade_notes_service.dart' as _i15; -import 'package:stackwallet/services/trade_service.dart' as _i13; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i10; +import 'package:stackwallet/services/trade_notes_service.dart' as _i16; +import 'package:stackwallet/services/trade_service.dart' as _i14; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i11; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i8; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i6; import 'package:stackwallet/utilities/prefs.dart' as _i5; import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart' - as _i11; + as _i12; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i2; @@ -557,6 +558,45 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i9.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i9.Level.all, + ) as _i9.Level); + + @override + set logLevel(_i9.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -564,67 +604,67 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ) as bool); @override - _i9.Future init() => (super.noSuchMethod( + _i10.Future init() => (super.noSuchMethod( Invocation.method( #init, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( + _i10.Future incrementCurrentNotificationIndex() => (super.noSuchMethod( Invocation.method( #incrementCurrentNotificationIndex, [], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future isExternalCallsSet() => (super.noSuchMethod( + _i10.Future isExternalCallsSet() => (super.noSuchMethod( Invocation.method( #isExternalCallsSet, [], ), - returnValue: _i9.Future.value(false), - ) as _i9.Future); + returnValue: _i10.Future.value(false), + ) as _i10.Future); @override - _i9.Future saveUserID(String? userId) => (super.noSuchMethod( + _i10.Future saveUserID(String? userId) => (super.noSuchMethod( Invocation.method( #saveUserID, [userId], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( + _i10.Future saveSignupEpoch(int? signupEpoch) => (super.noSuchMethod( Invocation.method( #saveSignupEpoch, [signupEpoch], ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i10.AmountUnit amountUnit(_i11.CryptoCurrency? coin) => (super.noSuchMethod( + _i11.AmountUnit amountUnit(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i10.AmountUnit.normal, - ) as _i10.AmountUnit); + returnValue: _i11.AmountUnit.normal, + ) as _i11.AmountUnit); @override void updateAmountUnit({ - required _i11.CryptoCurrency? coin, - required _i10.AmountUnit? amountUnit, + required _i12.CryptoCurrency? coin, + required _i11.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -639,7 +679,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - int maxDecimals(_i11.CryptoCurrency? coin) => (super.noSuchMethod( + int maxDecimals(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #maxDecimals, [coin], @@ -649,7 +689,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { @override void updateMaxDecimals({ - required _i11.CryptoCurrency? coin, + required _i12.CryptoCurrency? coin, required int? maxDecimals, }) => super.noSuchMethod( @@ -665,7 +705,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - _i2.FusionInfo getFusionServerInfo(_i11.CryptoCurrency? coin) => + _i2.FusionInfo getFusionServerInfo(_i12.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getFusionServerInfo, @@ -682,7 +722,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { @override void setFusionServerInfo( - _i11.CryptoCurrency? coin, + _i12.CryptoCurrency? coin, _i2.FusionInfo? fusionServerInfo, ) => super.noSuchMethod( @@ -697,7 +737,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -706,7 +746,7 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -736,16 +776,16 @@ class MockPrefs extends _i1.Mock implements _i5.Prefs { /// A class which mocks [TradesService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTradesService extends _i1.Mock implements _i13.TradesService { +class MockTradesService extends _i1.Mock implements _i14.TradesService { MockTradesService() { _i1.throwOnMissingStub(this); } @override - List<_i14.Trade> get trades => (super.noSuchMethod( + List<_i15.Trade> get trades => (super.noSuchMethod( Invocation.getter(#trades), - returnValue: <_i14.Trade>[], - ) as List<_i14.Trade>); + returnValue: <_i15.Trade>[], + ) as List<_i15.Trade>); @override bool get hasListeners => (super.noSuchMethod( @@ -754,14 +794,14 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { ) as bool); @override - _i14.Trade? get(String? tradeId) => (super.noSuchMethod(Invocation.method( + _i15.Trade? get(String? tradeId) => (super.noSuchMethod(Invocation.method( #get, [tradeId], - )) as _i14.Trade?); + )) as _i15.Trade?); @override - _i9.Future add({ - required _i14.Trade? trade, + _i10.Future add({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -773,13 +813,13 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future edit({ - required _i14.Trade? trade, + _i10.Future edit({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -791,13 +831,13 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future delete({ - required _i14.Trade? trade, + _i10.Future delete({ + required _i15.Trade? trade, required bool? shouldNotifyListeners, }) => (super.noSuchMethod( @@ -809,12 +849,12 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future deleteByUuid({ + _i10.Future deleteByUuid({ required String? uuid, required bool? shouldNotifyListeners, }) => @@ -827,12 +867,12 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { #shouldNotifyListeners: shouldNotifyListeners, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -841,7 +881,7 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -871,7 +911,7 @@ class MockTradesService extends _i1.Mock implements _i13.TradesService { /// A class which mocks [TradeNotesService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { +class MockTradeNotesService extends _i1.Mock implements _i16.TradeNotesService { MockTradeNotesService() { _i1.throwOnMissingStub(this); } @@ -906,7 +946,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { ) as String); @override - _i9.Future set({ + _i10.Future set({ required String? tradeId, required String? note, }) => @@ -919,23 +959,23 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { #note: note, }, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - _i9.Future delete({required String? tradeId}) => (super.noSuchMethod( + _i10.Future delete({required String? tradeId}) => (super.noSuchMethod( Invocation.method( #delete, [], {#tradeId: tradeId}, ), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) as _i9.Future); + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); @override - void addListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -944,7 +984,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { ); @override - void removeListener(_i12.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i13.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -974,7 +1014,7 @@ class MockTradeNotesService extends _i1.Mock implements _i15.TradeNotesService { /// A class which mocks [ChangeNowAPI]. /// /// See the documentation for Mockito's code generation for more information. -class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { +class MockChangeNowAPI extends _i1.Mock implements _i17.ChangeNowAPI { MockChangeNowAPI() { _i1.throwOnMissingStub(this); } @@ -989,54 +1029,55 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { ) as _i3.HTTP); @override - _i9.Future<_i4.ExchangeResponse>> getAvailableCurrencies({ + _i10.Future<_i4.ExchangeResponse>> + getAvailableCurrencies({ bool? fixedRate, bool? active, }) => - (super.noSuchMethod( - Invocation.method( - #getAvailableCurrencies, - [], - { - #fixedRate: fixedRate, - #active: active, - }, - ), - returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( - this, - Invocation.method( - #getAvailableCurrencies, - [], - { - #fixedRate: fixedRate, - #active: active, - }, - ), - )), - ) as _i9.Future<_i4.ExchangeResponse>>); + (super.noSuchMethod( + Invocation.method( + #getAvailableCurrencies, + [], + { + #fixedRate: fixedRate, + #active: active, + }, + ), + returnValue: + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( + this, + Invocation.method( + #getAvailableCurrencies, + [], + { + #fixedRate: fixedRate, + #active: active, + }, + ), + )), + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse>> getCurrenciesV2() => + _i10.Future<_i4.ExchangeResponse>> getCurrenciesV2() => (super.noSuchMethod( Invocation.method( #getCurrenciesV2, [], ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getCurrenciesV2, [], ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse>> getPairedCurrencies({ + _i10.Future<_i4.ExchangeResponse>> getPairedCurrencies({ required String? ticker, bool? fixedRate, }) => @@ -1050,8 +1091,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getPairedCurrencies, @@ -1062,10 +1103,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse<_i18.Decimal>> getMinimalExchangeAmount({ + _i10.Future<_i4.ExchangeResponse<_i19.Decimal>> getMinimalExchangeAmount({ required String? fromTicker, required String? toTicker, String? apiKey, @@ -1080,8 +1121,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i18.Decimal>>.value( - _FakeExchangeResponse_2<_i18.Decimal>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i19.Decimal>>.value( + _FakeExchangeResponse_2<_i19.Decimal>( this, Invocation.method( #getMinimalExchangeAmount, @@ -1093,10 +1134,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i18.Decimal>>); + ) as _i10.Future<_i4.ExchangeResponse<_i19.Decimal>>); @override - _i9.Future<_i4.ExchangeResponse<_i19.Range>> getRange({ + _i10.Future<_i4.ExchangeResponse<_i20.Range>> getRange({ required String? fromTicker, required String? toTicker, required bool? isFixedRate, @@ -1113,8 +1154,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i19.Range>>.value( - _FakeExchangeResponse_2<_i19.Range>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i20.Range>>.value( + _FakeExchangeResponse_2<_i20.Range>( this, Invocation.method( #getRange, @@ -1127,13 +1168,13 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i19.Range>>); + ) as _i10.Future<_i4.ExchangeResponse<_i20.Range>>); @override - _i9.Future<_i4.ExchangeResponse<_i20.Estimate>> getEstimatedExchangeAmount({ + _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmount({ required String? fromTicker, required String? toTicker, - required _i18.Decimal? fromAmount, + required _i19.Decimal? fromAmount, String? apiKey, }) => (super.noSuchMethod( @@ -1147,8 +1188,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>.value( - _FakeExchangeResponse_2<_i20.Estimate>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( + _FakeExchangeResponse_2<_i21.Estimate>( this, Invocation.method( #getEstimatedExchangeAmount, @@ -1161,14 +1202,14 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i9.Future<_i4.ExchangeResponse<_i20.Estimate>> + _i10.Future<_i4.ExchangeResponse<_i21.Estimate>> getEstimatedExchangeAmountFixedRate({ required String? fromTicker, required String? toTicker, - required _i18.Decimal? fromAmount, + required _i19.Decimal? fromAmount, required bool? reversed, bool? useRateId = true, String? apiKey, @@ -1186,8 +1227,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>.value( - _FakeExchangeResponse_2<_i20.Estimate>( + returnValue: _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>.value( + _FakeExchangeResponse_2<_i21.Estimate>( this, Invocation.method( #getEstimatedExchangeAmountFixedRate, @@ -1202,18 +1243,18 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i20.Estimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i21.Estimate>>); @override - _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>> + _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>> getEstimatedExchangeAmountV2({ required String? fromTicker, required String? toTicker, - required _i21.CNEstimateType? fromOrTo, - required _i18.Decimal? amount, + required _i22.CNEstimateType? fromOrTo, + required _i19.Decimal? amount, String? fromNetwork, String? toNetwork, - _i21.CNFlowType? flow = _i21.CNFlowType.standard, + _i22.CNFlowType? flow = _i22.CNFlowType.standard, String? apiKey, }) => (super.noSuchMethod( @@ -1231,9 +1272,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: - _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>>.value( - _FakeExchangeResponse_2<_i21.CNExchangeEstimate>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>.value( + _FakeExchangeResponse_2<_i22.CNExchangeEstimate>( this, Invocation.method( #getEstimatedExchangeAmountV2, @@ -1250,19 +1291,19 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i21.CNExchangeEstimate>>); + ) as _i10.Future<_i4.ExchangeResponse<_i22.CNExchangeEstimate>>); @override - _i9.Future<_i4.ExchangeResponse>> + _i10.Future<_i4.ExchangeResponse>> getAvailableFixedRateMarkets({String? apiKey}) => (super.noSuchMethod( Invocation.method( #getAvailableFixedRateMarkets, [], {#apiKey: apiKey}, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + returnValue: _i10 + .Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getAvailableFixedRateMarkets, @@ -1270,15 +1311,15 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#apiKey: apiKey}, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); @override - _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>> + _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> createStandardExchangeTransaction({ required String? fromTicker, required String? toTicker, required String? receivingAddress, - required _i18.Decimal? amount, + required _i19.Decimal? amount, String? extraId = r'', String? userId = r'', String? contactEmail = r'', @@ -1303,9 +1344,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i23.ExchangeTransaction>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i24.ExchangeTransaction>( this, Invocation.method( #createStandardExchangeTransaction, @@ -1324,15 +1365,15 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); @override - _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>> + _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>> createFixedRateExchangeTransaction({ required String? fromTicker, required String? toTicker, required String? receivingAddress, - required _i18.Decimal? amount, + required _i19.Decimal? amount, required String? rateId, required bool? reversed, String? extraId = r'', @@ -1361,9 +1402,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>.value( - _FakeExchangeResponse_2<_i23.ExchangeTransaction>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>.value( + _FakeExchangeResponse_2<_i24.ExchangeTransaction>( this, Invocation.method( #createFixedRateExchangeTransaction, @@ -1384,12 +1425,12 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i23.ExchangeTransaction>>); + ) as _i10.Future<_i4.ExchangeResponse<_i24.ExchangeTransaction>>); @override - _i9.Future< + _i10.Future< _i4 - .ExchangeResponse<_i24.ExchangeTransactionStatus>> getTransactionStatus({ + .ExchangeResponse<_i25.ExchangeTransactionStatus>> getTransactionStatus({ required String? id, String? apiKey, }) => @@ -1402,9 +1443,9 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { #apiKey: apiKey, }, ), - returnValue: _i9 - .Future<_i4.ExchangeResponse<_i24.ExchangeTransactionStatus>>.value( - _FakeExchangeResponse_2<_i24.ExchangeTransactionStatus>( + returnValue: _i10 + .Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>.value( + _FakeExchangeResponse_2<_i25.ExchangeTransactionStatus>( this, Invocation.method( #getTransactionStatus, @@ -1415,10 +1456,10 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { }, ), )), - ) as _i9.Future<_i4.ExchangeResponse<_i24.ExchangeTransactionStatus>>); + ) as _i10.Future<_i4.ExchangeResponse<_i25.ExchangeTransactionStatus>>); @override - _i9.Future<_i4.ExchangeResponse>> + _i10.Future<_i4.ExchangeResponse>> getAvailableFloatingRatePairs({bool? includePartners = false}) => (super.noSuchMethod( Invocation.method( @@ -1427,8 +1468,8 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#includePartners: includePartners}, ), returnValue: - _i9.Future<_i4.ExchangeResponse>>.value( - _FakeExchangeResponse_2>( + _i10.Future<_i4.ExchangeResponse>>.value( + _FakeExchangeResponse_2>( this, Invocation.method( #getAvailableFloatingRatePairs, @@ -1436,5 +1477,5 @@ class MockChangeNowAPI extends _i1.Mock implements _i16.ChangeNowAPI { {#includePartners: includePartners}, ), )), - ) as _i9.Future<_i4.ExchangeResponse>>); + ) as _i10.Future<_i4.ExchangeResponse>>); } diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index c2bc00d2c..218b9c0c5 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -5,19 +5,20 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i10; import 'dart:typed_data' as _i15; -import 'dart:ui' as _i20; +import 'dart:ui' as _i21; +import 'package:logger/logger.dart' as _i19; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i17; import 'package:stackwallet/db/isar/main_db.dart' as _i3; import 'package:stackwallet/models/isar/stack_theme.dart' as _i14; -import 'package:stackwallet/models/node_model.dart' as _i22; +import 'package:stackwallet/models/node_model.dart' as _i23; import 'package:stackwallet/networking/http.dart' as _i6; -import 'package:stackwallet/services/locale_service.dart' as _i21; +import 'package:stackwallet/services/locale_service.dart' as _i22; import 'package:stackwallet/services/node_service.dart' as _i2; import 'package:stackwallet/services/wallets.dart' as _i9; import 'package:stackwallet/themes/theme_service.dart' as _i13; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i19; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i18; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i16; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -833,6 +834,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i19.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i19.Level.all, + ) as _i19.Level); + + @override + set logLevel(_i19.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -889,18 +929,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i19.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i19.AmountUnit.normal, - ) as _i19.AmountUnit); + returnValue: _i20.AmountUnit.normal, + ) as _i20.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i19.AmountUnit? amountUnit, + required _i20.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -973,7 +1013,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -982,7 +1022,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1012,7 +1052,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { /// A class which mocks [LocaleService]. /// /// See the documentation for Mockito's code generation for more information. -class MockLocaleService extends _i1.Mock implements _i21.LocaleService { +class MockLocaleService extends _i1.Mock implements _i22.LocaleService { MockLocaleService() { _i1.throwOnMissingStub(this); } @@ -1044,7 +1084,7 @@ class MockLocaleService extends _i1.Mock implements _i21.LocaleService { ) as _i10.Future); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1053,7 +1093,7 @@ class MockLocaleService extends _i1.Mock implements _i21.LocaleService { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1098,16 +1138,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i8.SecureStorageInterface); @override - List<_i22.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i23.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override - List<_i22.NodeModel> get nodes => (super.noSuchMethod( + List<_i23.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( @@ -1128,7 +1168,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, - required _i22.NodeModel? node, + required _i23.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -1146,33 +1186,33 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - _i22.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => + _i23.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#currency: currency}, - )) as _i22.NodeModel?); + )) as _i23.NodeModel?); @override - List<_i22.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => + List<_i23.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override - _i22.NodeModel? getNodeById({required String? id}) => + _i23.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i22.NodeModel?); + )) as _i23.NodeModel?); @override - List<_i22.NodeModel> failoverNodesFor( + List<_i23.NodeModel> failoverNodesFor( {required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( Invocation.method( @@ -1180,12 +1220,12 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { [], {#currency: currency}, ), - returnValue: <_i22.NodeModel>[], - ) as List<_i22.NodeModel>); + returnValue: <_i23.NodeModel>[], + ) as List<_i23.NodeModel>); @override _i10.Future add( - _i22.NodeModel? node, + _i23.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -1240,7 +1280,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future edit( - _i22.NodeModel? editedNode, + _i23.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -1268,7 +1308,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - void addListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1277,7 +1317,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ); @override - void removeListener(_i20.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i21.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], diff --git a/test/widget_tests/node_options_sheet_test.mocks.dart b/test/widget_tests/node_options_sheet_test.mocks.dart index 5e878015f..cf7098de6 100644 --- a/test/widget_tests/node_options_sheet_test.mocks.dart +++ b/test/widget_tests/node_options_sheet_test.mocks.dart @@ -5,18 +5,19 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i10; import 'dart:io' as _i8; -import 'dart:ui' as _i17; +import 'dart:ui' as _i18; +import 'package:logger/logger.dart' as _i16; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i14; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/node_model.dart' as _i18; +import 'package:stackwallet/models/node_model.dart' as _i19; import 'package:stackwallet/services/event_bus/events/global/tor_connection_status_changed_event.dart' - as _i20; + as _i21; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/tor_service.dart' as _i19; +import 'package:stackwallet/services/tor_service.dart' as _i20; import 'package:stackwallet/services/wallets.dart' as _i9; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i16; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i17; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i15; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i13; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -28,7 +29,7 @@ import 'package:stackwallet/wallets/isar/models/wallet_info.dart' as _i11; import 'package:stackwallet/wallets/wallet/wallet.dart' as _i5; import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart' as _i6; -import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i21; +import 'package:tor_ffi_plugin/tor_ffi_plugin.dart' as _i22; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -708,6 +709,45 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i16.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i16.Level.all, + ) as _i16.Level); + + @override + set logLevel(_i16.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -764,18 +804,18 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ) as _i10.Future); @override - _i16.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i17.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i16.AmountUnit.normal, - ) as _i16.AmountUnit); + returnValue: _i17.AmountUnit.normal, + ) as _i17.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i16.AmountUnit? amountUnit, + required _i17.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -848,7 +888,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -857,7 +897,7 @@ class MockPrefs extends _i1.Mock implements _i12.Prefs { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -902,16 +942,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i7.SecureStorageInterface); @override - List<_i18.NodeModel> get primaryNodes => (super.noSuchMethod( + List<_i19.NodeModel> get primaryNodes => (super.noSuchMethod( Invocation.getter(#primaryNodes), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override - List<_i18.NodeModel> get nodes => (super.noSuchMethod( + List<_i19.NodeModel> get nodes => (super.noSuchMethod( Invocation.getter(#nodes), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override bool get hasListeners => (super.noSuchMethod( @@ -932,7 +972,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future setPrimaryNodeFor({ required _i4.CryptoCurrency? coin, - required _i18.NodeModel? node, + required _i19.NodeModel? node, bool? shouldNotifyListeners = false, }) => (super.noSuchMethod( @@ -950,33 +990,33 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - _i18.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => + _i19.NodeModel? getPrimaryNodeFor({required _i4.CryptoCurrency? currency}) => (super.noSuchMethod(Invocation.method( #getPrimaryNodeFor, [], {#currency: currency}, - )) as _i18.NodeModel?); + )) as _i19.NodeModel?); @override - List<_i18.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => + List<_i19.NodeModel> getNodesFor(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getNodesFor, [coin], ), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override - _i18.NodeModel? getNodeById({required String? id}) => + _i19.NodeModel? getNodeById({required String? id}) => (super.noSuchMethod(Invocation.method( #getNodeById, [], {#id: id}, - )) as _i18.NodeModel?); + )) as _i19.NodeModel?); @override - List<_i18.NodeModel> failoverNodesFor( + List<_i19.NodeModel> failoverNodesFor( {required _i4.CryptoCurrency? currency}) => (super.noSuchMethod( Invocation.method( @@ -984,12 +1024,12 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { [], {#currency: currency}, ), - returnValue: <_i18.NodeModel>[], - ) as List<_i18.NodeModel>); + returnValue: <_i19.NodeModel>[], + ) as List<_i19.NodeModel>); @override _i10.Future add( - _i18.NodeModel? node, + _i19.NodeModel? node, String? password, bool? shouldNotifyListeners, ) => @@ -1044,7 +1084,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { @override _i10.Future edit( - _i18.NodeModel? editedNode, + _i19.NodeModel? editedNode, String? password, bool? shouldNotifyListeners, ) => @@ -1072,7 +1112,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ) as _i10.Future); @override - void addListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1081,7 +1121,7 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { ); @override - void removeListener(_i17.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i18.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1111,16 +1151,16 @@ class MockNodeService extends _i1.Mock implements _i2.NodeService { /// A class which mocks [TorService]. /// /// See the documentation for Mockito's code generation for more information. -class MockTorService extends _i1.Mock implements _i19.TorService { +class MockTorService extends _i1.Mock implements _i20.TorService { MockTorService() { _i1.throwOnMissingStub(this); } @override - _i20.TorConnectionStatus get status => (super.noSuchMethod( + _i21.TorConnectionStatus get status => (super.noSuchMethod( Invocation.getter(#status), - returnValue: _i20.TorConnectionStatus.disconnected, - ) as _i20.TorConnectionStatus); + returnValue: _i21.TorConnectionStatus.disconnected, + ) as _i21.TorConnectionStatus); @override ({_i8.InternetAddress host, int port}) getProxyInfo() => (super.noSuchMethod( @@ -1143,7 +1183,7 @@ class MockTorService extends _i1.Mock implements _i19.TorService { @override void init({ required String? torDataDirPath, - _i21.Tor? mockableOverride, + _i22.Tor? mockableOverride, }) => super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index a5816bdcb..a146a9abf 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -4,27 +4,28 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i11; -import 'dart:typed_data' as _i25; +import 'dart:typed_data' as _i26; import 'dart:ui' as _i17; -import 'package:decimal/decimal.dart' as _i22; +import 'package:decimal/decimal.dart' as _i23; import 'package:isar/isar.dart' as _i9; +import 'package:logger/logger.dart' as _i20; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:stackwallet/db/isar/main_db.dart' as _i3; -import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i27; +import 'package:stackwallet/models/isar/models/block_explorer.dart' as _i28; import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart' - as _i29; -import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i26; -import 'package:stackwallet/models/isar/models/isar_models.dart' as _i28; -import 'package:stackwallet/models/isar/stack_theme.dart' as _i24; + as _i30; +import 'package:stackwallet/models/isar/models/contact_entry.dart' as _i27; +import 'package:stackwallet/models/isar/models/isar_models.dart' as _i29; +import 'package:stackwallet/models/isar/stack_theme.dart' as _i25; import 'package:stackwallet/networking/http.dart' as _i8; import 'package:stackwallet/services/locale_service.dart' as _i15; import 'package:stackwallet/services/node_service.dart' as _i2; -import 'package:stackwallet/services/price_service.dart' as _i21; +import 'package:stackwallet/services/price_service.dart' as _i22; import 'package:stackwallet/services/wallets.dart' as _i10; -import 'package:stackwallet/themes/theme_service.dart' as _i23; -import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i20; +import 'package:stackwallet/themes/theme_service.dart' as _i24; +import 'package:stackwallet/utilities/amount/amount_unit.dart' as _i21; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart' as _i19; import 'package:stackwallet/utilities/enums/sync_type_enum.dart' as _i18; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart' @@ -817,6 +818,45 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { returnValueForMissingStub: null, ); + @override + bool get advancedFiroFeatures => (super.noSuchMethod( + Invocation.getter(#advancedFiroFeatures), + returnValue: false, + ) as bool); + + @override + set advancedFiroFeatures(bool? advancedFiroFeatures) => super.noSuchMethod( + Invocation.setter( + #advancedFiroFeatures, + advancedFiroFeatures, + ), + returnValueForMissingStub: null, + ); + + @override + set logsPath(String? logsPath) => super.noSuchMethod( + Invocation.setter( + #logsPath, + logsPath, + ), + returnValueForMissingStub: null, + ); + + @override + _i20.Level get logLevel => (super.noSuchMethod( + Invocation.getter(#logLevel), + returnValue: _i20.Level.all, + ) as _i20.Level); + + @override + set logLevel(_i20.Level? logLevel) => super.noSuchMethod( + Invocation.setter( + #logLevel, + logLevel, + ), + returnValueForMissingStub: null, + ); + @override bool get hasListeners => (super.noSuchMethod( Invocation.getter(#hasListeners), @@ -873,18 +913,18 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { ) as _i11.Future); @override - _i20.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( + _i21.AmountUnit amountUnit(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #amountUnit, [coin], ), - returnValue: _i20.AmountUnit.normal, - ) as _i20.AmountUnit); + returnValue: _i21.AmountUnit.normal, + ) as _i21.AmountUnit); @override void updateAmountUnit({ required _i4.CryptoCurrency? coin, - required _i20.AmountUnit? amountUnit, + required _i21.AmountUnit? amountUnit, }) => super.noSuchMethod( Invocation.method( @@ -996,7 +1036,7 @@ class MockPrefs extends _i1.Mock implements _i14.Prefs { /// A class which mocks [PriceService]. /// /// See the documentation for Mockito's code generation for more information. -class MockPriceService extends _i1.Mock implements _i21.PriceService { +class MockPriceService extends _i1.Mock implements _i22.PriceService { MockPriceService() { _i1.throwOnMissingStub(this); } @@ -1042,36 +1082,36 @@ class MockPriceService extends _i1.Mock implements _i21.PriceService { ) as bool); @override - _i7.Tuple2<_i22.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => + _i7.Tuple2<_i23.Decimal, double> getPrice(_i4.CryptoCurrency? coin) => (super.noSuchMethod( Invocation.method( #getPrice, [coin], ), - returnValue: _FakeTuple2_5<_i22.Decimal, double>( + returnValue: _FakeTuple2_5<_i23.Decimal, double>( this, Invocation.method( #getPrice, [coin], ), ), - ) as _i7.Tuple2<_i22.Decimal, double>); + ) as _i7.Tuple2<_i23.Decimal, double>); @override - _i7.Tuple2<_i22.Decimal, double> getTokenPrice(String? contractAddress) => + _i7.Tuple2<_i23.Decimal, double> getTokenPrice(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getTokenPrice, [contractAddress], ), - returnValue: _FakeTuple2_5<_i22.Decimal, double>( + returnValue: _FakeTuple2_5<_i23.Decimal, double>( this, Invocation.method( #getTokenPrice, [contractAddress], ), ), - ) as _i7.Tuple2<_i22.Decimal, double>); + ) as _i7.Tuple2<_i23.Decimal, double>); @override _i11.Future updatePrice() => (super.noSuchMethod( @@ -1141,7 +1181,7 @@ class MockPriceService extends _i1.Mock implements _i21.PriceService { /// A class which mocks [ThemeService]. /// /// See the documentation for Mockito's code generation for more information. -class MockThemeService extends _i1.Mock implements _i23.ThemeService { +class MockThemeService extends _i1.Mock implements _i24.ThemeService { MockThemeService() { _i1.throwOnMissingStub(this); } @@ -1174,10 +1214,10 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ) as _i3.MainDB); @override - List<_i24.StackTheme> get installedThemes => (super.noSuchMethod( + List<_i25.StackTheme> get installedThemes => (super.noSuchMethod( Invocation.getter(#installedThemes), - returnValue: <_i24.StackTheme>[], - ) as List<_i24.StackTheme>); + returnValue: <_i25.StackTheme>[], + ) as List<_i25.StackTheme>); @override void init(_i3.MainDB? db) => super.noSuchMethod( @@ -1189,7 +1229,7 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ); @override - _i11.Future install({required _i25.Uint8List? themeArchiveData}) => + _i11.Future install({required _i26.Uint8List? themeArchiveData}) => (super.noSuchMethod( Invocation.method( #install, @@ -1233,35 +1273,35 @@ class MockThemeService extends _i1.Mock implements _i23.ThemeService { ) as _i11.Future); @override - _i11.Future> fetchThemes() => + _i11.Future> fetchThemes() => (super.noSuchMethod( Invocation.method( #fetchThemes, [], ), - returnValue: _i11.Future>.value( - <_i23.StackThemeMetaData>[]), - ) as _i11.Future>); + returnValue: _i11.Future>.value( + <_i24.StackThemeMetaData>[]), + ) as _i11.Future>); @override - _i11.Future<_i25.Uint8List> fetchTheme( - {required _i23.StackThemeMetaData? themeMetaData}) => + _i11.Future<_i26.Uint8List> fetchTheme( + {required _i24.StackThemeMetaData? themeMetaData}) => (super.noSuchMethod( Invocation.method( #fetchTheme, [], {#themeMetaData: themeMetaData}, ), - returnValue: _i11.Future<_i25.Uint8List>.value(_i25.Uint8List(0)), - ) as _i11.Future<_i25.Uint8List>); + returnValue: _i11.Future<_i26.Uint8List>.value(_i26.Uint8List(0)), + ) as _i11.Future<_i26.Uint8List>); @override - _i24.StackTheme? getTheme({required String? themeId}) => + _i25.StackTheme? getTheme({required String? themeId}) => (super.noSuchMethod(Invocation.method( #getTheme, [], {#themeId: themeId}, - )) as _i24.StackTheme?); + )) as _i25.StackTheme?); } /// A class which mocks [MainDB]. @@ -1314,13 +1354,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - List<_i26.ContactEntry> getContactEntries() => (super.noSuchMethod( + List<_i27.ContactEntry> getContactEntries() => (super.noSuchMethod( Invocation.method( #getContactEntries, [], ), - returnValue: <_i26.ContactEntry>[], - ) as List<_i26.ContactEntry>); + returnValue: <_i27.ContactEntry>[], + ) as List<_i27.ContactEntry>); @override _i11.Future deleteContactEntry({required String? id}) => @@ -1345,16 +1385,16 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i26.ContactEntry? getContactEntry({required String? id}) => + _i27.ContactEntry? getContactEntry({required String? id}) => (super.noSuchMethod(Invocation.method( #getContactEntry, [], {#id: id}, - )) as _i26.ContactEntry?); + )) as _i27.ContactEntry?); @override _i11.Future putContactEntry( - {required _i26.ContactEntry? contactEntry}) => + {required _i27.ContactEntry? contactEntry}) => (super.noSuchMethod( Invocation.method( #putContactEntry, @@ -1365,17 +1405,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i27.TransactionBlockExplorer? getTransactionBlockExplorer( + _i28.TransactionBlockExplorer? getTransactionBlockExplorer( {required _i4.CryptoCurrency? cryptoCurrency}) => (super.noSuchMethod(Invocation.method( #getTransactionBlockExplorer, [], {#cryptoCurrency: cryptoCurrency}, - )) as _i27.TransactionBlockExplorer?); + )) as _i28.TransactionBlockExplorer?); @override _i11.Future putTransactionBlockExplorer( - _i27.TransactionBlockExplorer? explorer) => + _i28.TransactionBlockExplorer? explorer) => (super.noSuchMethod( Invocation.method( #putTransactionBlockExplorer, @@ -1385,13 +1425,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i9.QueryBuilder<_i28.Address, _i28.Address, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause> getAddresses(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddresses, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.Address, _i28.Address, + returnValue: _FakeQueryBuilder_8<_i29.Address, _i29.Address, _i9.QAfterWhereClause>( this, Invocation.method( @@ -1400,10 +1440,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.Address, _i28.Address, _i9.QAfterWhereClause>); + .QueryBuilder<_i29.Address, _i29.Address, _i9.QAfterWhereClause>); @override - _i11.Future putAddress(_i28.Address? address) => (super.noSuchMethod( + _i11.Future putAddress(_i29.Address? address) => (super.noSuchMethod( Invocation.method( #putAddress, [address], @@ -1412,7 +1452,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future> putAddresses(List<_i28.Address>? addresses) => + _i11.Future> putAddresses(List<_i29.Address>? addresses) => (super.noSuchMethod( Invocation.method( #putAddresses, @@ -1422,7 +1462,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future> updateOrPutAddresses(List<_i28.Address>? addresses) => + _i11.Future> updateOrPutAddresses(List<_i29.Address>? addresses) => (super.noSuchMethod( Invocation.method( #updateOrPutAddresses, @@ -1432,7 +1472,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future<_i28.Address?> getAddress( + _i11.Future<_i29.Address?> getAddress( String? walletId, String? address, ) => @@ -1444,13 +1484,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _i11.Future<_i28.Address?>.value(), - ) as _i11.Future<_i28.Address?>); + returnValue: _i11.Future<_i29.Address?>.value(), + ) as _i11.Future<_i29.Address?>); @override _i11.Future updateAddress( - _i28.Address? oldAddress, - _i28.Address? newAddress, + _i29.Address? oldAddress, + _i29.Address? newAddress, ) => (super.noSuchMethod( Invocation.method( @@ -1464,13 +1504,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i9.QueryBuilder<_i28.Transaction, _i28.Transaction, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause> getTransactions(String? walletId) => (super.noSuchMethod( Invocation.method( #getTransactions, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.Transaction, _i28.Transaction, + returnValue: _FakeQueryBuilder_8<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause>( this, Invocation.method( @@ -1478,11 +1518,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), ), - ) as _i9.QueryBuilder<_i28.Transaction, _i28.Transaction, + ) as _i9.QueryBuilder<_i29.Transaction, _i29.Transaction, _i9.QAfterWhereClause>); @override - _i11.Future putTransaction(_i28.Transaction? transaction) => + _i11.Future putTransaction(_i29.Transaction? transaction) => (super.noSuchMethod( Invocation.method( #putTransaction, @@ -1493,7 +1533,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future> putTransactions( - List<_i28.Transaction>? transactions) => + List<_i29.Transaction>? transactions) => (super.noSuchMethod( Invocation.method( #putTransactions, @@ -1503,7 +1543,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i11.Future<_i28.Transaction?> getTransaction( + _i11.Future<_i29.Transaction?> getTransaction( String? walletId, String? txid, ) => @@ -1515,11 +1555,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i28.Transaction?>.value(), - ) as _i11.Future<_i28.Transaction?>); + returnValue: _i11.Future<_i29.Transaction?>.value(), + ) as _i11.Future<_i29.Transaction?>); @override - _i11.Stream<_i28.Transaction?> watchTransaction({ + _i11.Stream<_i29.Transaction?> watchTransaction({ required int? id, bool? fireImmediately = false, }) => @@ -1532,11 +1572,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.Transaction?>.empty(), - ) as _i11.Stream<_i28.Transaction?>); + returnValue: _i11.Stream<_i29.Transaction?>.empty(), + ) as _i11.Stream<_i29.Transaction?>); @override - _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause> getUTXOs( + _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause> getUTXOs( String? walletId) => (super.noSuchMethod( Invocation.method( @@ -1544,17 +1584,17 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { [walletId], ), returnValue: - _FakeQueryBuilder_8<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause>( + _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>( this, Invocation.method( #getUTXOs, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterWhereClause>); + ) as _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterWhereClause>); @override - _i9.QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterFilterCondition> + _i9.QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition> getUTXOsByAddress( String? walletId, String? address, @@ -1567,7 +1607,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { address, ], ), - returnValue: _FakeQueryBuilder_8<_i28.UTXO, _i28.UTXO, + returnValue: _FakeQueryBuilder_8<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>( this, Invocation.method( @@ -1579,10 +1619,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.UTXO, _i28.UTXO, _i9.QAfterFilterCondition>); + .QueryBuilder<_i29.UTXO, _i29.UTXO, _i9.QAfterFilterCondition>); @override - _i11.Future putUTXO(_i28.UTXO? utxo) => (super.noSuchMethod( + _i11.Future putUTXO(_i29.UTXO? utxo) => (super.noSuchMethod( Invocation.method( #putUTXO, [utxo], @@ -1592,7 +1632,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future putUTXOs(List<_i28.UTXO>? utxos) => (super.noSuchMethod( + _i11.Future putUTXOs(List<_i29.UTXO>? utxos) => (super.noSuchMethod( Invocation.method( #putUTXOs, [utxos], @@ -1604,7 +1644,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future updateUTXOs( String? walletId, - List<_i28.UTXO>? utxos, + List<_i29.UTXO>? utxos, ) => (super.noSuchMethod( Invocation.method( @@ -1618,7 +1658,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Stream<_i28.UTXO?> watchUTXO({ + _i11.Stream<_i29.UTXO?> watchUTXO({ required int? id, bool? fireImmediately = false, }) => @@ -1631,11 +1671,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.UTXO?>.empty(), - ) as _i11.Stream<_i28.UTXO?>); + returnValue: _i11.Stream<_i29.UTXO?>.empty(), + ) as _i11.Stream<_i29.UTXO?>); @override - _i9.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, _i9.QAfterWhereClause> getTransactionNotes( String? walletId) => (super.noSuchMethod( @@ -1643,19 +1683,19 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #getTransactionNotes, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.TransactionNote, - _i28.TransactionNote, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_8<_i29.TransactionNote, + _i29.TransactionNote, _i9.QAfterWhereClause>( this, Invocation.method( #getTransactionNotes, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.TransactionNote, _i28.TransactionNote, + ) as _i9.QueryBuilder<_i29.TransactionNote, _i29.TransactionNote, _i9.QAfterWhereClause>); @override - _i11.Future putTransactionNote(_i28.TransactionNote? transactionNote) => + _i11.Future putTransactionNote(_i29.TransactionNote? transactionNote) => (super.noSuchMethod( Invocation.method( #putTransactionNote, @@ -1667,7 +1707,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future putTransactionNotes( - List<_i28.TransactionNote>? transactionNotes) => + List<_i29.TransactionNote>? transactionNotes) => (super.noSuchMethod( Invocation.method( #putTransactionNotes, @@ -1678,7 +1718,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future<_i28.TransactionNote?> getTransactionNote( + _i11.Future<_i29.TransactionNote?> getTransactionNote( String? walletId, String? txid, ) => @@ -1690,11 +1730,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { txid, ], ), - returnValue: _i11.Future<_i28.TransactionNote?>.value(), - ) as _i11.Future<_i28.TransactionNote?>); + returnValue: _i11.Future<_i29.TransactionNote?>.value(), + ) as _i11.Future<_i29.TransactionNote?>); @override - _i11.Stream<_i28.TransactionNote?> watchTransactionNote({ + _i11.Stream<_i29.TransactionNote?> watchTransactionNote({ required int? id, bool? fireImmediately = false, }) => @@ -1707,29 +1747,29 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.TransactionNote?>.empty(), - ) as _i11.Stream<_i28.TransactionNote?>); + returnValue: _i11.Stream<_i29.TransactionNote?>.empty(), + ) as _i11.Stream<_i29.TransactionNote?>); @override - _i9.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, _i9.QAfterWhereClause> + _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause> getAddressLabels(String? walletId) => (super.noSuchMethod( Invocation.method( #getAddressLabels, [walletId], ), - returnValue: _FakeQueryBuilder_8<_i28.AddressLabel, - _i28.AddressLabel, _i9.QAfterWhereClause>( + returnValue: _FakeQueryBuilder_8<_i29.AddressLabel, + _i29.AddressLabel, _i9.QAfterWhereClause>( this, Invocation.method( #getAddressLabels, [walletId], ), ), - ) as _i9.QueryBuilder<_i28.AddressLabel, _i28.AddressLabel, + ) as _i9.QueryBuilder<_i29.AddressLabel, _i29.AddressLabel, _i9.QAfterWhereClause>); @override - _i11.Future putAddressLabel(_i28.AddressLabel? addressLabel) => + _i11.Future putAddressLabel(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabel, @@ -1739,7 +1779,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - int putAddressLabelSync(_i28.AddressLabel? addressLabel) => + int putAddressLabelSync(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #putAddressLabelSync, @@ -1749,7 +1789,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as int); @override - _i11.Future putAddressLabels(List<_i28.AddressLabel>? addressLabels) => + _i11.Future putAddressLabels(List<_i29.AddressLabel>? addressLabels) => (super.noSuchMethod( Invocation.method( #putAddressLabels, @@ -1760,7 +1800,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future<_i28.AddressLabel?> getAddressLabel( + _i11.Future<_i29.AddressLabel?> getAddressLabel( String? walletId, String? addressString, ) => @@ -1772,11 +1812,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { addressString, ], ), - returnValue: _i11.Future<_i28.AddressLabel?>.value(), - ) as _i11.Future<_i28.AddressLabel?>); + returnValue: _i11.Future<_i29.AddressLabel?>.value(), + ) as _i11.Future<_i29.AddressLabel?>); @override - _i28.AddressLabel? getAddressLabelSync( + _i29.AddressLabel? getAddressLabelSync( String? walletId, String? addressString, ) => @@ -1786,10 +1826,10 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { walletId, addressString, ], - )) as _i28.AddressLabel?); + )) as _i29.AddressLabel?); @override - _i11.Stream<_i28.AddressLabel?> watchAddressLabel({ + _i11.Stream<_i29.AddressLabel?> watchAddressLabel({ required int? id, bool? fireImmediately = false, }) => @@ -1802,11 +1842,11 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { #fireImmediately: fireImmediately, }, ), - returnValue: _i11.Stream<_i28.AddressLabel?>.empty(), - ) as _i11.Stream<_i28.AddressLabel?>); + returnValue: _i11.Stream<_i29.AddressLabel?>.empty(), + ) as _i11.Stream<_i29.AddressLabel?>); @override - _i11.Future updateAddressLabel(_i28.AddressLabel? addressLabel) => + _i11.Future updateAddressLabel(_i29.AddressLabel? addressLabel) => (super.noSuchMethod( Invocation.method( #updateAddressLabel, @@ -1850,7 +1890,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future addNewTransactionData( - List<_i7.Tuple2<_i28.Transaction, _i28.Address?>>? transactionsData, + List<_i7.Tuple2<_i29.Transaction, _i29.Address?>>? transactionsData, String? walletId, ) => (super.noSuchMethod( @@ -1867,7 +1907,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { @override _i11.Future> updateOrPutTransactionV2s( - List<_i29.TransactionV2>? transactions) => + List<_i30.TransactionV2>? transactions) => (super.noSuchMethod( Invocation.method( #updateOrPutTransactionV2s, @@ -1877,13 +1917,13 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future>); @override - _i9.QueryBuilder<_i28.EthContract, _i28.EthContract, _i9.QWhere> + _i9.QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere> getEthContracts() => (super.noSuchMethod( Invocation.method( #getEthContracts, [], ), - returnValue: _FakeQueryBuilder_8<_i28.EthContract, _i28.EthContract, + returnValue: _FakeQueryBuilder_8<_i29.EthContract, _i29.EthContract, _i9.QWhere>( this, Invocation.method( @@ -1892,27 +1932,27 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ), ), ) as _i9 - .QueryBuilder<_i28.EthContract, _i28.EthContract, _i9.QWhere>); + .QueryBuilder<_i29.EthContract, _i29.EthContract, _i9.QWhere>); @override - _i11.Future<_i28.EthContract?> getEthContract(String? contractAddress) => + _i11.Future<_i29.EthContract?> getEthContract(String? contractAddress) => (super.noSuchMethod( Invocation.method( #getEthContract, [contractAddress], ), - returnValue: _i11.Future<_i28.EthContract?>.value(), - ) as _i11.Future<_i28.EthContract?>); + returnValue: _i11.Future<_i29.EthContract?>.value(), + ) as _i11.Future<_i29.EthContract?>); @override - _i28.EthContract? getEthContractSync(String? contractAddress) => + _i29.EthContract? getEthContractSync(String? contractAddress) => (super.noSuchMethod(Invocation.method( #getEthContractSync, [contractAddress], - )) as _i28.EthContract?); + )) as _i29.EthContract?); @override - _i11.Future putEthContract(_i28.EthContract? contract) => + _i11.Future putEthContract(_i29.EthContract? contract) => (super.noSuchMethod( Invocation.method( #putEthContract, @@ -1922,7 +1962,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { ) as _i11.Future); @override - _i11.Future putEthContracts(List<_i28.EthContract>? contracts) => + _i11.Future putEthContracts(List<_i29.EthContract>? contracts) => (super.noSuchMethod( Invocation.method( #putEthContracts, @@ -1947,7 +1987,7 @@ class MockMainDB extends _i1.Mock implements _i3.MainDB { /// A class which mocks [IThemeAssets]. /// /// See the documentation for Mockito's code generation for more information. -class MockIThemeAssets extends _i1.Mock implements _i24.IThemeAssets { +class MockIThemeAssets extends _i1.Mock implements _i25.IThemeAssets { MockIThemeAssets() { _i1.throwOnMissingStub(this); } From 09da845ae9de13acc11080c9285e612387173076 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 12 Feb 2025 19:49:47 -0600 Subject: [PATCH 02/29] WIP names --- .../isar/models/blockchain_data/utxo.dart | 1 + .../confirm_name_transaction_view.dart | 1067 +++++++++++++++++ .../namecoin_names_home_view.dart | 350 ++++++ lib/pages/wallet_view/wallet_view.dart | 13 + .../sub_widgets/desktop_wallet_features.dart | 11 + .../more_features/more_features_dialog.dart | 10 + lib/route_generator.dart | 31 + lib/wallets/models/name_op_state.dart | 52 + lib/wallets/models/tx_data.dart | 8 + lib/wallets/wallet/impl/namecoin_wallet.dart | 925 +++++++++++++- lib/wallets/wallet/wallet.dart | 26 +- .../electrumx_interface.dart | 25 +- lib/widgets/desktop/desktop_fee_dialog.dart | 2 +- pubspec.lock | 9 + scripts/app_config/templates/pubspec.template | 4 + 15 files changed, 2498 insertions(+), 36 deletions(-) create mode 100644 lib/pages/namecoin_names/confirm_name_transaction_view.dart create mode 100644 lib/pages/namecoin_names/namecoin_names_home_view.dart create mode 100644 lib/wallets/models/name_op_state.dart diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index 77de5ae3a..e4f91258c 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -180,4 +180,5 @@ class UTXO { abstract final class UTXOOtherDataKeys { static const keyImage = "keyImage"; static const spent = "spent"; + static const nameOpData = "nameOpData"; } diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart new file mode 100644 index 000000000..f46fcf795 --- /dev/null +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -0,0 +1,1067 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'dart:async'; +import 'dart:io'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../models/isar/models/transaction_note.dart'; +import '../../notifications/show_flush_bar.dart'; +import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; +import '../../pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; +import '../../providers/db/main_db_provider.dart'; +import '../../providers/global/secure_store_provider.dart'; +import '../../providers/providers.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../themes/theme_providers.dart'; +import '../../utilities/amount/amount.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/models/tx_data.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/icon_widgets/x_icon.dart'; +import '../../widgets/rounded_container.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import '../../widgets/stack_text_field.dart'; +import '../../widgets/textfield_icon_button.dart'; +import '../pinpad_views/lock_screen_view.dart'; +import '../send_view/sub_widgets/sending_transaction_dialog.dart'; + +class ConfirmNameTransactionView extends ConsumerStatefulWidget { + const ConfirmNameTransactionView({ + super.key, + required this.txData, + required this.walletId, + }); + + static const String routeName = "/confirmNameTransactionView"; + + final TxData txData; + final String walletId; + + @override + ConsumerState createState() => + _ConfirmNameTransactionViewState(); +} + +class _ConfirmNameTransactionViewState + extends ConsumerState { + late final String walletId; + late final bool isDesktop; + + late final FocusNode _noteFocusNode; + late final TextEditingController noteController; + + Future _attemptSend() async { + final wallet = ref.read(pWallets).getWallet(walletId); + final coin = wallet.info.coin; + + final sendProgressController = ProgressAndSuccessController(); + + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return SendingTransactionDialog( + coin: coin, + controller: sendProgressController, + ); + }, + ), + ); + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final List txids = []; + Future txDataFuture; + + final note = noteController.text; + + try { + txDataFuture = wallet.confirmSend(txData: widget.txData); + + // await futures in parallel + final futureResults = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = (futureResults.first as TxData); + + sendProgressController.triggerSuccess?.call(); + + // await futures in parallel + await Future.wait([ + // wait for animation + Future.delayed(const Duration(seconds: 5)), + + // associated name data for reg tx + ref.read(secureStoreProvider).write( + key: nameSaltKeyBuilder(txData.txid!, walletId), + value: encodeNameSaltData( + txData.opNameState!.name, + txData.opNameState!.saltHex, + txData.opNameState!.value, + ), + ), + ]); + + txids.add(txData.txid!); + ref.refresh(desktopUseUTXOs); + + // save note + for (final txid in txids) { + await ref.read(mainDBProvider).putTransactionNote( + TransactionNote( + walletId: walletId, + txid: txid, + value: note, + ), + ); + } + + unawaited(wallet.refresh()); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop confirm send view + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + } + } catch (e, s) { + const niceError = "Broadcast name transaction failed"; + + Logging.instance.e(niceError, error: e, stackTrace: s); + + if (mounted) { + // pop sending dialog + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + niceError, + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Flexible( + child: SingleChildScrollView( + child: SelectableText( + e.toString(), + style: STextStyles.smallMed14(context), + ), + ), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ), + ], + ), + ), + ); + } else { + return StackDialog( + title: niceError, + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } + }, + ); + } + } + } + + @override + void initState() { + isDesktop = Util.isDesktop; + walletId = widget.walletId; + _noteFocusNode = FocusNode(); + noteController = TextEditingController(); + noteController.text = widget.txData.note ?? ""; + + super.initState(); + } + + @override + void dispose() { + noteController.dispose(); + + _noteFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(walletId)); + + final unit = coin.ticker; + + final fee = widget.txData.fee; + final amountWithoutChange = widget.txData.amountWithoutChange!; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + // if (FocusScope.of(context).hasFocus) { + // FocusScope.of(context).unfocus(); + // await Future.delayed(Duration(milliseconds: 50)); + // } + Navigator.of(context).pop(); + }, + ), + title: Text( + "Confirm transaction", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + AppBarBackButton( + size: 40, + iconSize: 24, + onPressed: () => Navigator.of( + context, + rootNavigator: true, + ).pop(), + ), + Text( + "Confirm transaction", + style: STextStyles.desktopH3(context), + ), + ], + ), + Flexible( + child: SingleChildScrollView( + child: child, + ), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Confirm Name transaction", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Name", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.opNameState!.name, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Value", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.opNameState!.value, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Recipient", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format( + amountWithoutChange, + ), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction fee", + style: STextStyles.smallMed12(context), + ), + SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle12(context), + textAlign: TextAlign.right, + ), + ], + ), + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + const SizedBox( + height: 12, + ), + if (widget.txData.fee != null && widget.txData.vSize != null) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "sats/vByte", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + "~${fee.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + if (widget.txData.note!.isNotEmpty) + const SizedBox( + height: 12, + ), + if (widget.txData.note!.isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + SelectableText( + widget.txData.note!, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ), + ], + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + right: 32, + bottom: 50, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + borderColor: + Theme.of(context).extension()!.background, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .background, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 22, + ), + child: Row( + children: [ + SvgPicture.file( + File( + ref.watch( + themeProvider.select( + (value) => value.assets.send, + ), + ), + ), + width: 32, + height: 32, + ), + const SizedBox( + width: 16, + ), + Text( + "Send $unit Name transaction", + style: STextStyles.desktopTextMedium(context), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Name", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + widget.txData.opNameState!.name, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + Container( + height: 1, + color: Theme.of(context) + .extension()! + .background, + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Value", + style: STextStyles.desktopTextExtraExtraSmall( + context, + ), + ), + const SizedBox( + height: 2, + ), + SelectableText( + widget.txData.opNameState!.value, + style: STextStyles.desktopTextExtraExtraSmall( + context, + ).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + ], + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText( + "Note (optional)", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + autocorrect: isDesktop ? false : true, + enableSuggestions: isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState( + () => noteController.text = "", + ); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 20, + ), + ], + ), + ), + + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Amount", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Builder( + builder: (context) { + final externalCalls = ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.externalCalls, + ), + ); + String fiatAmount = "N/A"; + + if (externalCalls) { + final price = ref + .read( + priceAnd24hChangeNotifierProvider, + ) + .getPrice(coin) + .item1; + if (price > Decimal.zero) { + fiatAmount = (amountWithoutChange.decimal * price) + .toAmount(fractionDigits: 2) + .fiatString( + locale: ref + .read( + localeServiceChangeNotifierProvider, + ) + .locale, + ); + } + } + + return Row( + children: [ + SelectableText( + ref.watch(pAmountFormatter(coin)).format( + amountWithoutChange, + ), + style: STextStyles.itemSubtitle( + context, + ), + ), + if (externalCalls) + Text( + " | ", + style: STextStyles.itemSubtitle( + context, + ), + ), + if (externalCalls) + SelectableText( + "~$fiatAmount ${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: STextStyles.itemSubtitle( + context, + ), + ), + ], + ); + }, + ), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Recipient", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + widget.txData.recipients!.first.address, + style: STextStyles.itemSubtitle(context), + ), + ), + ), + // todo amoutn here + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "Transaction fee", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(fee!), + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only( + top: 16, + left: 32, + ), + child: Text( + "sats/vByte", + style: STextStyles.desktopTextExtraExtraSmall(context), + ), + ), + if (isDesktop && + widget.txData.fee != null && + widget.txData.vSize != null) + Padding( + padding: const EdgeInsets.only( + top: 10, + left: 32, + right: 32, + ), + child: RoundedContainer( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ), + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: SelectableText( + "~${fee!.raw.toInt() ~/ widget.txData.vSize!}", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 23 : 12, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 18, + ) + : const EdgeInsets.all(12), + color: Theme.of(context) + .extension()! + .snackBarBackSuccess, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isDesktop ? "Total amount to send" : "Total amount", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.titleBold12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + ), + SelectableText( + ref + .watch(pAmountFormatter(coin)) + .format(amountWithoutChange + fee!), + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ), + ], + ), + ), + ), + SizedBox( + height: isDesktop ? 28 : 16, + ), + Padding( + padding: isDesktop + ? const EdgeInsets.symmetric( + horizontal: 32, + ) + : const EdgeInsets.all(0), + child: PrimaryButton( + label: "Send", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + final dynamic unlocked; + + if (isDesktop) { + unlocked = await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopAuthSend( + coin: coin, + ), + ), + ], + ), + ), + ); + } else { + unlocked = await Navigator.push( + context, + RouteGenerator.getRoute( + shouldUseMaterialRoute: + RouteGenerator.useMaterialPageRoute, + builder: (_) => const LockscreenView( + showBackButton: true, + popOnSuccess: true, + routeOnSuccessArguments: true, + routeOnSuccess: "", + biometricsCancelButtonString: "CANCEL", + biometricsLocalizedReason: + "Authenticate to send transaction", + biometricsAuthenticationTitle: "Confirm Transaction", + ), + settings: + const RouteSettings(name: "/confirmsendlockscreen"), + ), + ); + } + + if (mounted) { + if (unlocked == true) { + unawaited(_attemptSend()); + } else { + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.warning, + message: Util.isDesktop + ? "Invalid passphrase" + : "Invalid PIN", + context: context, + ), + ); + } + } + } + }, + ), + ), + if (isDesktop) + const SizedBox( + height: 32, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart new file mode 100644 index 000000000..531d543a3 --- /dev/null +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -0,0 +1,350 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../models/isar/models/blockchain_data/utxo.dart'; +import '../../providers/db/main_db_provider.dart'; +import '../../providers/global/wallets_provider.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/amount/amount.dart'; +import '../../utilities/assets.dart'; +import '../../utilities/enums/fee_rate_type_enum.dart'; +import '../../utilities/logger.dart'; +import '../../utilities/show_loading.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../wallets/models/name_op_state.dart'; +import '../../wallets/models/tx_data.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_dialog.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; +import '../../widgets/desktop/primary_button.dart'; +import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/rounded_white_container.dart'; +import '../../widgets/stack_dialog.dart'; +import 'confirm_name_transaction_view.dart'; + +class NamecoinNamesHomeView extends ConsumerStatefulWidget { + const NamecoinNamesHomeView({ + super.key, + required this.walletId, + }); + + final String walletId; + + static const String routeName = "/namecoinNamesHomeView"; + + @override + ConsumerState createState() => + _NamecoinNamesHomeViewState(); +} + +class _NamecoinNamesHomeViewState extends ConsumerState { + String? lastAvailableName; + + NamecoinWallet get _wallet => + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + Future _preRegister() async { + final data = scriptNameNew(lastAvailableName!); + + // TODO: fill out properly + TxData txData = TxData( + opNameState: NameOpState( + name: lastAvailableName!, + saltHex: data.$2, + commitment: data.$3, + value: "test", // TODO: get from user for automatic reg later + nameScriptHex: data.$1, + type: OpName.nameNew, + ), + feeRateType: FeeRateType.slow, // TODO: make configurable? + recipients: [ + ( + address: (await _wallet.getCurrentReceivingAddress())!.value, + isChange: false, + amount: Amount.fromDecimal( + Decimal.parse("0.015"), + fractionDigits: _wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + txData = await _wallet.prepareNameSend(txData: txData); + + Logging.instance.f("SALTY: ${txData.opNameState!.saltHex}"); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: _wallet.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, _wallet.walletId), + ); + } + } + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final isDesktop = Util.isDesktop; + + return MasterScaffold( + isDesktop: isDesktop, + appBar: isDesktop + ? DesktopAppBar( + isCompactHeight: true, + background: Theme.of(context).extension()!.popupBG, + leading: Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 24, + right: 20, + ), + child: AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + ), + SvgPicture.asset( + Assets.svg.file, + width: 32, + height: 32, + color: Theme.of(context).extension()!.textDark, + ), + const SizedBox( + width: 10, + ), + Text( + "Names", + style: STextStyles.desktopH3(context), + ), + ], + ), + ) + : AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Names", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: ConditionalParent( + condition: !isDesktop, + builder: (child) => SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + child: Column( + crossAxisAlignment: + isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + LookupNameForm( + walletId: widget.walletId, + onNameAvailable: (name) { + if (name != lastAvailableName) { + setState(() { + lastAvailableName = name; + }); + } + }, + ), + if (lastAvailableName != null) + PrimaryButton( + label: "Register $lastAvailableName", + onPressed: _preRegister, + ), + const SizedBox( + height: 32, + ), + Expanded( + child: StreamBuilder( + stream: ref.watch( + mainDBProvider.select( + (s) => s.isar.utxos + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .otherDataIsNotNull() + .watch(fireImmediately: true), + ), + ), + builder: (context, snapshot) { + List list = []; + if (snapshot.hasData) { + list = snapshot.data!; + } + + return ListView.separated( + itemCount: list.length, + itemBuilder: (context, index) => RoundedWhiteContainer( + child: Text(list[index].otherData!), + ), + separatorBuilder: (context, index) => const SizedBox( + height: 10, + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} + +class LookupNameForm extends ConsumerStatefulWidget { + const LookupNameForm({ + super.key, + required this.walletId, + this.onNameAvailable, + }); + + final String walletId; + + final void Function(String? name)? onNameAvailable; + + @override + ConsumerState createState() => _LookupNameFormState(); +} + +class _LookupNameFormState extends ConsumerState { + final nameController = TextEditingController(); + final nameFieldFocus = FocusNode(); + + NamecoinWallet get _wallet => + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try { + widget.onNameAvailable?.call(null); + final result = await showLoading( + whileFuture: _wallet.lookupName(nameController.text), + context: context, + message: "Looking up ${nameController.text}", + onException: (e) => throw e, + rootNavigator: Util.isDesktop, + delay: const Duration(seconds: 2), + ); + + if (result?.available == true) { + widget.onNameAvailable?.call(nameController.text); + } + + Logging.instance.i("LOOKUP RESULT: $result"); + } catch (e, s) { + widget.onNameAvailable?.call(null); + Logging.instance.e("_lookup failed", error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Name lookup failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + nameController.dispose(); + nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + TextField( + textInputAction: TextInputAction.search, + focusNode: nameFieldFocus, + controller: nameController, + onSubmitted: (_) { + if (nameController.text.isNotEmpty) { + _lookup(); + } + }, + onChanged: (_) { + // trigger look up button enabled/disabled state change + setState(() {}); + }, + ), + const SizedBox( + height: 20, + ), + SecondaryButton( + label: "Look up name", + enabled: nameController.text.isNotEmpty, + width: 160, + buttonHeight: ButtonHeight.l, + onPressed: _lookup, + ), + ], + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index d94e25d7a..a4a846dcf 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -52,6 +52,7 @@ import '../../wallets/crypto_currency/intermediate/frost_currency.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/bitcoin_frost_wallet.dart'; import '../../wallets/wallet/impl/firo_wallet.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; @@ -87,6 +88,7 @@ import '../churning/churning_view.dart'; import '../coin_control/coin_control_view.dart'; import '../exchange_view/wallet_initiated_exchange_view.dart'; import '../monkey/monkey_view.dart'; +import '../namecoin_names/namecoin_names_home_view.dart'; import '../notification_views/notifications_view.dart'; import '../ordinals/ordinals_view.dart'; import '../paynym/paynym_claim_view.dart'; @@ -1172,6 +1174,17 @@ class _WalletViewState extends ConsumerState { ); }, ), + if (wallet is NamecoinWallet) + WalletNavigationBarItemData( + label: "Names", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + NamecoinNamesHomeView.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 7993ee0a6..74512e256 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 @@ -19,6 +19,7 @@ import 'package:flutter_svg/svg.dart'; import '../../../../app_config.dart'; import '../../../../notifications/show_flush_bar.dart'; import '../../../../pages/monkey/monkey_view.dart'; +import '../../../../pages/namecoin_names/namecoin_names_home_view.dart'; import '../../../../pages/paynym/paynym_claim_view.dart'; import '../../../../pages/paynym/paynym_home_view.dart'; import '../../../../providers/desktop/current_desktop_menu_item.dart'; @@ -99,6 +100,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { onMonkeyPressed: _onMonkeyPressed, onFusionPressed: _onFusionPressed, onChurnPressed: _onChurnPressed, + onNamesPressed: _onNamesPressed, ), ); } @@ -380,6 +382,15 @@ class _DesktopWalletFeaturesState extends ConsumerState { ); } + void _onNamesPressed() { + Navigator.of(context, rootNavigator: true).pop(); + + Navigator.of(context).pushNamed( + NamecoinNamesHomeView.routeName, + arguments: widget.walletId, + ); + } + @override Widget build(BuildContext context) { final wallet = ref.watch(pWallets).getWallet(widget.walletId); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index eb2746558..72a5d0778 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 @@ -31,6 +31,7 @@ import '../../../../../wallets/crypto_currency/crypto_currency.dart'; import '../../../../../wallets/isar/models/wallet_info.dart'; import '../../../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../../../wallets/wallet/impl/firo_wallet.dart'; +import '../../../../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../../../../wallets/wallet/intermediate/lib_monero_wallet.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart'; import '../../../../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; @@ -62,6 +63,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { required this.onMonkeyPressed, required this.onFusionPressed, required this.onChurnPressed, + required this.onNamesPressed, }); final String walletId; @@ -75,6 +77,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { final VoidCallback? onMonkeyPressed; final VoidCallback? onFusionPressed; final VoidCallback? onChurnPressed; + final VoidCallback? onNamesPressed; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -474,6 +477,13 @@ class _MoreFeaturesDialogState extends ConsumerState { iconAsset: Assets.svg.churn, onPressed: () async => widget.onChurnPressed?.call(), ), + if (wallet is NamecoinWallet) + _MoreFeaturesItem( + label: "Names", + detail: "Namecoin DNS", + iconAsset: Assets.svg.file, + onPressed: () async => widget.onNamesPressed?.call(), + ), if (wallet is SparkInterface && !isViewOnly) _MoreFeaturesClearSparkCacheItem( cryptoCurrency: wallet.cryptoCurrency, diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 71a3b890e..1a3581db1 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -72,6 +72,8 @@ import 'pages/home_view/home_view.dart'; import 'pages/intro_view.dart'; import 'pages/manage_favorites_view/manage_favorites_view.dart'; import 'pages/monkey/monkey_view.dart'; +import 'pages/namecoin_names/confirm_name_transaction_view.dart'; +import 'pages/namecoin_names/namecoin_names_home_view.dart'; import 'pages/notification_views/notifications_view.dart'; import 'pages/ordinals/ordinal_details_view.dart'; import 'pages/ordinals/ordinals_filter_view.dart'; @@ -772,6 +774,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case NamecoinNamesHomeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NamecoinNamesHomeView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FusionProgressView.routeName: if (args is String) { return getRoute( @@ -1843,6 +1859,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmNameTransactionView.routeName: + if (args is (TxData, String)) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ConfirmNameTransactionView( + txData: args.$1, + walletId: args.$2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletInitiatedExchangeView.routeName: if (args is Tuple2) { return getRoute( diff --git a/lib/wallets/models/name_op_state.dart b/lib/wallets/models/name_op_state.dart new file mode 100644 index 000000000..98ca00e2d --- /dev/null +++ b/lib/wallets/models/name_op_state.dart @@ -0,0 +1,52 @@ +import 'package:namecoin/namecoin.dart'; + +class NameOpState { + final String name; + + final OpName type; + + final String saltHex; + final String commitment; + final String value; + final String nameScriptHex; + + NameOpState({ + required this.name, + required this.type, + required this.saltHex, + required this.commitment, + required this.value, + required this.nameScriptHex, + }); + + NameOpState copyWith({ + String? walletId, + String? name, + String? txid, + OpName? type, + String? saltHex, + String? commitment, + String? value, + String? nameScriptHex, + }) { + return NameOpState( + name: name ?? this.name, + type: type ?? this.type, + saltHex: saltHex ?? this.saltHex, + commitment: commitment ?? this.commitment, + value: value ?? this.value, + nameScriptHex: nameScriptHex ?? this.nameScriptHex, + ); + } + + @override + String toString() { + return "NameOpState(" + "name: $name, " + "type: ${type.name}, " + "saltHex: $saltHex, " + "commitment: $commitment, " + "value: $value, " + "nameScriptHex: $nameScriptHex)"; + } +} diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart index 652a5605f..21fa206c9 100644 --- a/lib/wallets/models/tx_data.dart +++ b/lib/wallets/models/tx_data.dart @@ -8,6 +8,7 @@ import '../../models/paynym/paynym_account_lite.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/enums/fee_rate_type_enum.dart'; import '../isar/models/spark_coin.dart'; +import 'name_op_state.dart'; typedef TxRecipient = ({String address, Amount amount, bool isChange}); @@ -77,6 +78,9 @@ class TxData { final bool ignoreCachedBalanceChecks; + // Namecoin Name related + final NameOpState? opNameState; + TxData({ this.feeRateType, this.feeRateAmount, @@ -113,6 +117,7 @@ class TxData { this.usedSparkCoins, this.tempTx, this.ignoreCachedBalanceChecks = false, + this.opNameState, }); Amount? get amount { @@ -239,6 +244,7 @@ class TxData { List? usedSparkCoins, TransactionV2? tempTx, bool? ignoreCachedBalanceChecks, + NameOpState? opNameState, }) { return TxData( feeRateType: feeRateType ?? this.feeRateType, @@ -277,6 +283,7 @@ class TxData { tempTx: tempTx ?? this.tempTx, ignoreCachedBalanceChecks: ignoreCachedBalanceChecks ?? this.ignoreCachedBalanceChecks, + opNameState: opNameState ?? this.opNameState, ); } @@ -316,5 +323,6 @@ class TxData { 'usedSparkCoins: $usedSparkCoins, ' 'tempTx: $tempTx, ' 'ignoreCachedBalanceChecks: $ignoreCachedBalanceChecks, ' + 'opNameState: $opNameState, ' '}'; } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 5eb54b33b..a911215bb 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -1,17 +1,49 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; +import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; -import '../../../models/isar/models/blockchain_data/address.dart'; -import '../../../models/isar/models/blockchain_data/transaction.dart'; import '../../../models/isar/models/blockchain_data/v2/input_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/output_v2.dart'; import '../../../models/isar/models/blockchain_data/v2/transaction_v2.dart'; +import '../../../models/isar/models/isar_models.dart'; +import '../../../models/signing_data.dart'; import '../../../utilities/amount/amount.dart'; +import '../../../utilities/enums/derive_path_type_enum.dart'; +import '../../../utilities/enums/fee_rate_type_enum.dart'; +import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; import '../../crypto_currency/crypto_currency.dart'; import '../../crypto_currency/interfaces/electrumx_currency_interface.dart'; +import '../../models/name_op_state.dart'; +import '../../models/tx_data.dart'; import '../intermediate/bip39_hd_wallet.dart'; import '../wallet_mixin_interfaces/coin_control_interface.dart'; +import '../wallet_mixin_interfaces/cpfp_interface.dart'; import '../wallet_mixin_interfaces/electrumx_interface.dart'; +import '../wallet_mixin_interfaces/rbf_interface.dart'; + +const kNameWaitBlocks = blocksMinToRenewName; +const kNameTxVersion = 0x7100; + +const _kNameSaltSplitter = r"$$$$"; + +String nameSaltKeyBuilder(String txid, String walletId) => + "${walletId}_${txid}_nameSaltData"; + +String encodeNameSaltData(String name, String salt, String value) => + "$name$_kNameSaltSplitter$salt$_kNameSaltSplitter$value"; +({String salt, String name, String value}) decodeNameSaltData(String value) { + try { + final split = value.split(_kNameSaltSplitter); + return (salt: split[1], name: split[0], value: split[2]); + } catch (_) { + throw Exception("Bad name salt data"); + } +} class NamecoinWallet extends Bip39HDWallet @@ -50,19 +82,118 @@ class NamecoinWallet // =========================================================================== @override - Future< - ({ - bool blocked, - String? blockedReason, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map jsonUTXO, String? scriptPubKeyHex, Map jsonTX, String? utxoOwnerAddress, - ) async { - // Namecoin doesn't have special outputs like tokens, ordinals, etc. - return (blocked: false, blockedReason: null, utxoLabel: null); + ) { + throw UnsupportedError( + "Namecoin does not used the checkBlockUTXO() function. " + "Due to tight integration with names, output freezing is handled directly" + " in the overridden parseUTXO() function.", + ); + } + + @override + Future parseUTXO({ + required Map jsonUTXO, + }) async { + final txn = await electrumXCachedClient.getTransaction( + txHash: jsonUTXO["tx_hash"] as String, + verbose: true, + cryptoCurrency: cryptoCurrency, + ); + + final inputs = txn["vin"] as List? ?? []; + final isCoinbase = inputs.any((e) => (e as Map?)?["coinbase"] != null); + + final vout = jsonUTXO["tx_pos"] as int; + + final outputs = txn["vout"] as List; + + String? utxoOwnerAddress; + + bool shouldBlock = false; + String? blockReason; + String? label; + String? otherDataString; + + for (final output in outputs) { + // find matching output + if (output["n"] == vout) { + utxoOwnerAddress = + output["scriptPubKey"]?["addresses"]?[0] as String? ?? + output["scriptPubKey"]?["address"] as String?; + + // check for nameOp + if (output["scriptPubKey"]?["nameOp"] != null) { + // block/freeze regardless of whether parsing the raw data succeeds + shouldBlock = true; + blockReason = "Contains name"; + + try { + final rawNameOP = (output["scriptPubKey"]["nameOp"] as Map) + .cast(); + + otherDataString = jsonEncode({ + UTXOOtherDataKeys.nameOpData: jsonEncode(rawNameOP), + }); + final nameOp = OpNameData( + rawNameOP, + jsonUTXO["height"] as int, + ); + Logging.instance.i( + "nameOp:\n$nameOp", + ); + + switch (nameOp.op) { + case OpName.nameNew: + label = "Name New"; + break; + case OpName.nameFirstUpdate: + label = "Name First Update: ${nameOp.fullname}"; + break; + case OpName.nameUpdate: + label = "Name Update: ${nameOp.fullname}"; + break; + } + } catch (e, s) { + Logging.instance.w( + "Namecoin OpNameData failed to parse" + " \"${output["scriptPubKey"]?["nameOp"]}\"", + error: e, + stackTrace: s, + ); + label = "Failed to parse raw nameOp data"; + } + } + + break; + } + } + + final utxo = UTXO( + walletId: walletId, + txid: txn["txid"] as String, + vout: vout, + value: jsonUTXO["value"] as int, + name: label ?? "", + isBlocked: shouldBlock, + blockedReason: blockReason, + isCoinbase: txn["is_coinbase"] as bool? ?? + txn["is-coinbase"] as bool? ?? + txn["iscoinbase"] as bool? ?? + isCoinbase, + blockHash: txn["blockhash"] as String?, + blockHeight: jsonUTXO["height"] as int?, + blockTime: txn["blocktime"] as int?, + address: utxoOwnerAddress, + otherData: otherDataString, + ); + + return utxo; } @override @@ -242,7 +373,7 @@ class NamecoinWallet .fold(BigInt.zero, (value, element) => value + element); TransactionType type; - final TransactionSubType subType = TransactionSubType.none; + const TransactionSubType subType = TransactionSubType.none; // At least one input was owned by this wallet. if (wasSentFromThisWallet) { @@ -290,4 +421,774 @@ class NamecoinWallet await mainDB.updateOrPutTransactionV2s(txns); } + + // namecoin names ============================================================ + + Future<({OpNameData? data, bool available})> lookupName(String name) async { + bool available = false; + + final nameScriptHash = nameIdentifierToScriptHash(name); + + final historyWithName = await electrumXClient.getHistory( + scripthash: nameScriptHash, + ); + OpNameData? opNameData; + if (historyWithName.isNotEmpty) { + final txHeight = historyWithName.last["height"] as int; + final txHash = historyWithName.last["tx_hash"] as String; + + final txMap = await electrumXCachedClient.getTransaction( + txHash: txHash, + cryptoCurrency: cryptoCurrency, + ); + + try { + opNameData = OpNameData.fromTx(txMap, txHeight); + final isExpired = opNameData.expired(await chainHeight); + + Logging.instance.i( + "Name $opNameData \nis expired = $isExpired", + ); + available = isExpired; + } catch (_) { + available = false; // probably + } + } else { + Logging.instance.i("Name \"$name\" not found."); + available = true; + } + + return (data: opNameData, available: available); + } + + /// Must be called in refresh() AFTER the wallet's UTXOs have been updated! + Future checkForNameNewOPs() async { + final currentHeight = await chainHeight; + // not ideal filtering + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .and() + .blockHeightIsNotNull() + .and() + .blockHeightLessThan(currentHeight - kNameWaitBlocks) + .findAll(); + + for (final utxo in utxos) { + final otherData = jsonDecode(utxo.otherData!) as Map; + if (otherData[UTXOOtherDataKeys.nameOpData] != null) { + final nameOp = OpNameData( + (jsonDecode(otherData[UTXOOtherDataKeys.nameOpData] as String) as Map) + .cast(), + utxo.blockHeight!, + ); + + Logging.instance.t( + "Found OpName: $nameOp", + stackTrace: StackTrace.current, + ); + + if (nameOp.op == OpName.nameNew) { + // at this point we should have an unspent UTXO that is at least + // 12 blocks old which we can now do nameFirstUpdate on + + //TODO: Should check if name was registered by someone else here + + final sKey = nameSaltKeyBuilder(utxo.txid, walletId); + + final data = decodeNameSaltData( + (await secureStorageInterface.read(key: sKey))!, + ); + + final nameScriptHex = scriptNameFirstUpdate( + data.name, + data.value, + data.salt, + ); + + TxData txData = TxData( + opNameState: NameOpState( + name: data.name, + saltHex: data.salt, + commitment: "n/a", + value: data.value, + nameScriptHex: nameScriptHex, + type: OpName.nameFirstUpdate, + ), + feeRateType: FeeRateType.slow, // TODO: make configurable? + recipients: [ + ( + address: (await getCurrentReceivingAddress())!.value, + isChange: false, + amount: Amount.fromDecimal( + Decimal.parse("0.01"), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + txData = await prepareNameSend(txData: txData); + + txData = await confirmSend(txData: txData); + + // TODO + await secureStorageInterface.delete(key: sKey); + // TODO + await secureStorageInterface.write( + key: nameSaltKeyBuilder(txData.txid!, walletId), + value: encodeNameSaltData( + txData.opNameState!.name, + txData.opNameState!.saltHex, + txData.opNameState!.value, + ), + ); + } + } + } + } + + /// Builds and signs a transaction + Future _createNameTx({ + required TxData txData, + required List utxoSigningData, + required bool isForFeeCalcPurposesOnly, + }) async { + Logging.instance.d("Starting _createNameTx ----------"); + + assert(txData.recipients!.where((e) => !e.isChange).length == 1); + + if (!isForFeeCalcPurposesOnly) { + final nameAmount = + txData.recipients!.where((e) => !e.isChange).first.amount; + + switch (txData.opNameState!.type) { + case OpName.nameNew: + assert( + nameAmount.decimal.toString() == "0.015", + ); + break; + case OpName.nameFirstUpdate || OpName.nameUpdate: + assert( + nameAmount.decimal.toString() == "0.01", + ); + break; + } + } + + // temp tx data to show in gui while waiting for real data from server + final List tempInputs = []; + final List tempOutputs = []; + + final List prevOuts = []; + + coinlib.Transaction clTx = coinlib.Transaction( + version: kNameTxVersion, + inputs: [], + outputs: [], + ); + + // TODO: [prio=high]: check this opt in rbf + final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; + + // Add transaction inputs + for (var i = 0; i < utxoSigningData.length; i++) { + final txid = utxoSigningData[i].utxo.txid; + + final hash = Uint8List.fromList( + txid.toUint8ListFromHex.reversed.toList(), + ); + + final prevOutpoint = coinlib.OutPoint( + hash, + utxoSigningData[i].utxo.vout, + ); + + final prevOutput = coinlib.Output.fromAddress( + BigInt.from(utxoSigningData[i].utxo.value), + coinlib.Address.fromString( + utxoSigningData[i].utxo.address!, + cryptoCurrency.networkParams, + ), + ); + + prevOuts.add(prevOutput); + + final coinlib.Input input; + + switch (utxoSigningData[i].derivePathType) { + case DerivePathType.bip44: + input = coinlib.P2PKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: sequence, + ); + + // TODO: fix this as it is (probably) wrong! + case DerivePathType.bip49: + throw Exception("TODO p2sh"); + // input = coinlib.P2SHMultisigInput( + // prevOut: prevOutpoint, + // program: coinlib.MultisigProgram.decompile( + // utxoSigningData[i].redeemScript!, + // ), + // sequence: sequence, + // ); + + case DerivePathType.bip84: + input = coinlib.P2WPKHInput( + prevOut: prevOutpoint, + publicKey: utxoSigningData[i].keyPair!.publicKey, + sequence: sequence, + ); + + case DerivePathType.bip86: + input = coinlib.TaprootKeyInput(prevOut: prevOutpoint); + + default: + throw UnsupportedError( + "Unknown derivation path type found: ${utxoSigningData[i].derivePathType}", + ); + } + + clTx = clTx.addInput(input); + + tempInputs.add( + InputV2.isarCantDoRequiredInDefaultConstructor( + scriptSigHex: input.scriptSig.toHex, + scriptSigAsm: null, + sequence: sequence, + outpoint: OutpointV2.isarCantDoRequiredInDefaultConstructor( + txid: utxoSigningData[i].utxo.txid, + vout: utxoSigningData[i].utxo.vout, + ), + addresses: utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], + valueStringSats: utxoSigningData[i].utxo.value.toString(), + witness: null, + innerRedeemScriptAsm: null, + coinbase: null, + walletOwns: true, + ), + ); + } + + int nonChangeCount = 0; // sanity check counter. Should only hit 1. + // Add transaction outputs + for (var i = 0; i < txData.recipients!.length; i++) { + final address = coinlib.Address.fromString( + normalizeAddress(txData.recipients![i].address), + cryptoCurrency.networkParams, + ); + + final coinlib.Output output; + + // there should only be 1 name output + if (!txData.recipients![i].isChange) { + nonChangeCount++; + if (nonChangeCount > 1) { + Logging.instance.d("Oddly formatted Name txData: $txData"); + throw Exception("Oddly formatted Name tx"); + } + final scriptPubKey = address.program.script.compiled; + output = coinlib.Output.fromScriptBytes( + txData.recipients![i].amount.raw, // should be 0.015 or 0.01 + Uint8List.fromList( + txData.opNameState!.nameScriptHex.toUint8ListFromHex + scriptPubKey, + ), + ); + } else { + output = coinlib.Output.fromAddress( + txData.recipients![i].amount.raw, + address, + ); + } + + clTx = clTx.addOutput(output); + + tempOutputs.add( + OutputV2.isarCantDoRequiredInDefaultConstructor( + scriptPubKeyHex: "000000", + valueStringSats: txData.recipients![i].amount.raw.toString(), + addresses: [ + txData.recipients![i].address.toString(), + ], + walletOwns: (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .valueEqualTo(txData.recipients![i].address) + .valueProperty() + .findFirst()) != + null, + ), + ); + } + + try { + // Sign the transaction accordingly + for (var i = 0; i < utxoSigningData.length; i++) { + final value = BigInt.from(utxoSigningData[i].utxo.value); + coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + + if (clTx.inputs[i] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot( + internalKey: utxoSigningData[i].keyPair!.publicKey, + ); + + key = taproot.tweakPrivateKey(key); + } + + clTx = clTx.sign( + inputN: i, + value: value, + key: key, + prevOuts: prevOuts, + ); + } + } catch (e, s) { + Logging.instance.e( + "Caught exception while signing transaction: ", + error: e, + stackTrace: s, + ); + rethrow; + } + + return txData.copyWith( + raw: clTx.toHex(), + vSize: clTx.vSize(), + tempTx: TransactionV2( + walletId: walletId, + blockHash: null, + hash: clTx.hashHex, + txid: clTx.txid, + height: null, + timestamp: DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, + inputs: List.unmodifiable(tempInputs), + outputs: List.unmodifiable(tempOutputs), + version: clTx.version, + type: + tempOutputs.map((e) => e.walletOwns).fold(true, (p, e) => p &= e) && + txData.paynymAccountLite == null + ? TransactionType.sentToSelf + : TransactionType.outgoing, + subType: TransactionSubType.none, + otherData: null, + ), + ); + } + + Future prepareNameSend({ + required TxData txData, + }) async { + try { + if (txData.amount == null) { + throw Exception("No recipients in attempted transaction!"); + } + + final feeRateType = txData.feeRateType; + final customSatsPerVByte = txData.satsPerVByte; + final feeRateAmount = txData.feeRateAmount; + final utxos = txData.utxos; + + final bool coinControl = utxos != null; + + if (customSatsPerVByte != null) { + final result = await coinSelectionName( + txData: txData.copyWith(feeRateAmount: -1), + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance.d("PREPARE NAME SEND RESULT: $result"); + + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee cannot be less than vSize", + ); + } + + return result; + } else if (feeRateType is FeeRateType || feeRateAmount is int) { + late final int rate; + if (feeRateType is FeeRateType) { + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + default: + throw ArgumentError("Invalid use of custom fee"); + } + rate = fee; + } else { + rate = feeRateAmount as int; + } + + final result = await coinSelectionName( + txData: txData.copyWith( + feeRateAmount: rate, + ), + utxos: utxos?.toList(), + coinControl: coinControl, + ); + + Logging.instance.d( + "prepare send: $result", + ); + if (result.fee!.raw.toInt() < result.vSize!) { + throw Exception( + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})"); + } + + return result; + } else { + throw ArgumentError("Invalid fee rate argument provided!"); + } + } catch (e, s) { + Logging.instance.e( + "Exception rethrown from prepareNameSend(): ", + error: e, + stackTrace: s, + ); + rethrow; + } + } + + Future coinSelectionName({ + required TxData txData, + required bool coinControl, + int additionalOutputs = 0, + List? utxos, + }) async { + Logging.instance.d("Starting coinSelectionName ----------"); + + assert(txData.recipients!.length == 1); + + if (coinControl && utxos == null) { + throw Exception("Coin control used where utxos is null!"); + } + + final recipientAddress = txData.recipients!.first.address; + final satoshiAmountToSend = txData.amount!.raw; + final int? satsPerVByte = txData.satsPerVByte; + final selectedTxFeeRate = txData.feeRateAmount!; + + final int expectedSatsValue; + switch (txData.opNameState!.type) { + case OpName.nameNew: + expectedSatsValue = 150_0000; + break; + case OpName.nameFirstUpdate || OpName.nameUpdate: + expectedSatsValue = 100_0000; + break; + } + + if (satoshiAmountToSend != BigInt.from(expectedSatsValue)) { + throw Exception( + "Invalid Name amount for ${txData.opNameState!.type}: ${txData.amount}", + ); + } + + final List availableOutputs = + utxos ?? await mainDB.getUTXOs(walletId).findAll(); + final currentChainHeight = await chainHeight; + + final canCPFP = this is CpfpInterface && coinControl; + + final spendableOutputs = availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); + final spendableSatoshiValue = + spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); + + if (spendableSatoshiValue < satoshiAmountToSend) { + throw Exception("Insufficient balance"); + } else if (spendableSatoshiValue == satoshiAmountToSend) { + throw Exception("Insufficient balance to pay transaction fee"); + } + + if (coinControl) { + if (spendableOutputs.length < availableOutputs.length) { + throw ArgumentError("Attempted to use an unavailable utxo"); + } + // don't care about sorting if using all utxos + } else { + // sort spendable by age (oldest first) + spendableOutputs.sort( + (a, b) => (b.blockTime ?? currentChainHeight) + .compareTo((a.blockTime ?? currentChainHeight)), + ); + } + + Logging.instance.d( + "spendableOutputs.length: ${spendableOutputs.length}" + "\navailableOutputs.length: ${availableOutputs.length}" + "\nspendableOutputs: $spendableOutputs" + "\nspendableSatoshiValue: $spendableSatoshiValue" + "\nsatoshiAmountToSend: $satoshiAmountToSend", + ); + + BigInt satoshisBeingUsed = BigInt.zero; + int inputsBeingConsumed = 0; + final List utxoObjectsToUse = []; + + if (!coinControl) { + for (var i = 0; + satoshisBeingUsed < satoshiAmountToSend && + i < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[i]); + satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); + inputsBeingConsumed += 1; + } + for (int i = 0; + i < additionalOutputs && + inputsBeingConsumed < spendableOutputs.length; + i++) { + utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); + satoshisBeingUsed += + BigInt.from(spendableOutputs[inputsBeingConsumed].value); + inputsBeingConsumed += 1; + } + } else { + satoshisBeingUsed = spendableSatoshiValue; + utxoObjectsToUse.addAll(spendableOutputs); + inputsBeingConsumed = spendableOutputs.length; + } + + Logging.instance.d( + "satoshisBeingUsed: $satoshisBeingUsed" + "\ninputsBeingConsumed: $inputsBeingConsumed" + "\nutxoObjectsToUse: $utxoObjectsToUse", + ); + + // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray + final List recipientsArray = [recipientAddress]; + final List recipientsAmtArray = [satoshiAmountToSend]; + + // gather required signing data + final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse); + + final int vSizeForOneOutput; + try { + vSizeForOneOutput = (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed], + ), + ), + )) + .vSize!; + } catch (e, s) { + Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); + rethrow; + } + + final int vSizeForTwoOutPuts; + + BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; + + try { + vSizeForTwoOutPuts = (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await getCurrentChangeAddress())!.value], + [ + satoshiAmountToSend, + maxBI( + BigInt.zero, + satoshisBeingUsed - satoshiAmountToSend, + ), + ], + ), + ), + )) + .vSize!; + } catch (e, s) { + Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); + rethrow; + } + + // Assume 1 output, only for recipient and no change + final feeForOneOutput = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForOneOutput) + : estimateTxFee( + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), + ); + // Assume 2 outputs, one for recipient and one for change + final feeForTwoOutputs = BigInt.from( + satsPerVByte != null + ? (satsPerVByte * vSizeForTwoOutPuts) + : estimateTxFee( + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), + ); + + Logging.instance.d( + "feeForTwoOutputs: $feeForTwoOutputs" + "\nfeeForOneOutput: $feeForOneOutput", + ); + + final difference = satoshisBeingUsed - satoshiAmountToSend; + + Future _singleOutputTxn() async { + Logging.instance.d( + 'Input size: $satoshisBeingUsed' + '\nRecipient output size: $satoshiAmountToSend' + '\nFee being paid: $difference sats' + '\nEstimated fee: $feeForOneOutput', + ); + final txnData = await _createNameTx( + isForFeeCalcPurposesOnly: false, + utxoSigningData: utxoSigningData, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + return txnData.copyWith( + fee: Amount( + rawValue: feeForOneOutput, + fractionDigits: cryptoCurrency.fractionDigits, + ), + usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + ); + } + + // no change output required + if (difference == feeForOneOutput) { + Logging.instance.d('1 output in tx'); + return await _singleOutputTxn(); + } else if (difference < feeForOneOutput) { + Logging.instance.w( + 'Cannot pay tx fee - checking for more outputs and trying again', + ); + // try adding more outputs + if (spendableOutputs.length > inputsBeingConsumed) { + return coinSelectionName( + txData: txData, + additionalOutputs: additionalOutputs + 1, + utxos: utxos, + coinControl: coinControl, + ); + } + throw Exception("Insufficient balance to pay transaction fee"); + } else { + if (difference > (feeForOneOutput + cryptoCurrency.dustLimit.raw)) { + final changeOutputSize = difference - feeForTwoOutputs; + // check if possible to add the change output + if (changeOutputSize > cryptoCurrency.dustLimit.raw && + difference - changeOutputSize == feeForTwoOutputs) { + // generate new change address if current change address has been used + await checkChangeAddressForTransactions(); + final String newChangeAddress = + (await getCurrentChangeAddress())!.value; + + BigInt feeBeingPaid = difference - changeOutputSize; + + // add change output + recipientsArray.add(newChangeAddress); + recipientsAmtArray.add(changeOutputSize); + + Logging.instance.d('2 outputs in tx' + '\nInput size: $satoshisBeingUsed' + '\nRecipient output size: $satoshiAmountToSend' + '\nChange Output Size: $changeOutputSize' + '\nDifference (fee being paid): $feeBeingPaid sats' + '\nEstimated fee: $feeForTwoOutputs'); + + TxData txnData = await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: false, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + + // make sure minimum fee is accurate if that is being used + if (BigInt.from(txnData.vSize!) - feeBeingPaid == BigInt.one) { + final changeOutputSize = difference - BigInt.from(txnData.vSize!); + feeBeingPaid = difference - changeOutputSize; + recipientsAmtArray.removeLast(); + recipientsAmtArray.add(changeOutputSize); + + Logging.instance.d( + '\nAdjusted Input size: $satoshisBeingUsed' + '\nAdjusted Recipient output size: $satoshiAmountToSend' + '\nAdjusted Change Output Size: $changeOutputSize' + '\nAdjusted Difference (fee being paid): $feeBeingPaid sats' + '\nAdjusted Estimated fee: $feeForTwoOutputs', + ); + + txnData = await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: false, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + recipientsArray, + recipientsAmtArray, + ), + ), + ); + } + + return txnData.copyWith( + fee: Amount( + rawValue: feeBeingPaid, + fractionDigits: cryptoCurrency.fractionDigits, + ), + usedUTXOs: utxoSigningData.map((e) => e.utxo).toList(), + ); + } else { + // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize + // is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction. + Logging.instance.d( + 'Reverting to 1 output in tx', + ); + + return await _singleOutputTxn(); + } + } + } + + return txData; + } } diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 36cec7b85..9f74fbcee 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -663,17 +663,21 @@ abstract class Wallet { await (this as SparkInterface).refreshSparkData((0.3, 0.6)); } - final fetchFuture = updateTransactions(); - - _fireRefreshPercentChange(0.6); - final utxosRefreshFuture = updateUTXOs(); - // if (currentHeight != storedHeight) { - _fireRefreshPercentChange(0.65); - - await utxosRefreshFuture; - _fireRefreshPercentChange(0.70); - - await fetchFuture; + if (this is NamecoinWallet) { + await updateUTXOs(); + _fireRefreshPercentChange(0.6); + await (this as NamecoinWallet).checkForNameNewOPs(); + _fireRefreshPercentChange(0.70); + await updateTransactions(); + } else { + final fetchFuture = updateTransactions(); + _fireRefreshPercentChange(0.6); + final utxosRefreshFuture = updateUTXOs(); + _fireRefreshPercentChange(0.65); + await utxosRefreshFuture; + _fireRefreshPercentChange(0.70); + await fetchFuture; + } // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided. if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) { diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index d4b387b8d..4416b918a 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -74,7 +74,7 @@ mixin ElectrumXInterface } Future> - _helperRecipientsConvert( + helperRecipientsConvert( List addrs, List satValues, ) async { @@ -248,7 +248,7 @@ mixin ElectrumXInterface vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [satoshisBeingUsed - BigInt.one], ), @@ -268,7 +268,7 @@ mixin ElectrumXInterface vSizeForTwoOutPuts = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress, (await getCurrentChangeAddress())!.value], [ satoshiAmountToSend, @@ -330,7 +330,7 @@ mixin ElectrumXInterface final txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -392,7 +392,7 @@ mixin ElectrumXInterface TxData txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -425,7 +425,7 @@ mixin ElectrumXInterface txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( recipientsArray, recipientsAmtArray, ), @@ -474,7 +474,7 @@ mixin ElectrumXInterface final int vSizeForOneOutput = (await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [satoshisBeingUsed - BigInt.one], ), @@ -511,7 +511,7 @@ mixin ElectrumXInterface final data = await buildTransaction( txData: txData.copyWith( - recipients: await _helperRecipientsConvert( + recipients: await helperRecipientsConvert( [recipientAddress], [amount], ), @@ -1155,8 +1155,6 @@ mixin ElectrumXInterface } } - /// The optional (nullable) param [checkBlock] is a callback that can be used - /// to check if a utxo should be marked as blocked Future parseUTXO({ required Map jsonUTXO, }) async { @@ -1685,8 +1683,11 @@ mixin ElectrumXInterface return await mainDB.updateUTXOs(walletId, outputArray); } catch (e, s) { - Logging.instance - .e("Output fetch unsuccessful: ", error: e, stackTrace: s); + Logging.instance.e( + "Output fetch unsuccessful: ", + error: e, + stackTrace: s, + ); return false; } } diff --git a/lib/widgets/desktop/desktop_fee_dialog.dart b/lib/widgets/desktop/desktop_fee_dialog.dart index 322fbb876..713aaa109 100644 --- a/lib/widgets/desktop/desktop_fee_dialog.dart +++ b/lib/widgets/desktop/desktop_fee_dialog.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:cs_monero/cs_monero.dart' as lib_monero; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/models.dart'; diff --git a/pubspec.lock b/pubspec.lock index 590466016..d5280cd24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1385,6 +1385,15 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0" + namecoin: + dependency: "direct main" + description: + path: "." + ref: "41c4f32eb0eb82540a42fae25a10ece2557d517c" + resolved-ref: "41c4f32eb0eb82540a42fae25a10ece2557d517c" + url: "https://github.com/Cyrix126/namecoin_dart" + source: git + version: "0.3.0" nanodart: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 5a2b8e6cd..2e83d017d 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -214,6 +214,10 @@ dependencies: git: url: https://github.com/cypherstack/logger ref: 3c0cba27868ebb5c7d65ebc30a8e6e5342186692 + namecoin: + git: + url: https://github.com/Cyrix126/namecoin_dart + ref: 41c4f32eb0eb82540a42fae25a10ece2557d517c dev_dependencies: flutter_test: From 80a8e463e538509dbcf17540419ae211fe62f257 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 12:01:57 -0600 Subject: [PATCH 03/29] update namecoin_dart version --- pubspec.lock | 6 +++--- scripts/app_config/templates/pubspec.template | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index d5280cd24..233e824f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1389,11 +1389,11 @@ packages: dependency: "direct main" description: path: "." - ref: "41c4f32eb0eb82540a42fae25a10ece2557d517c" - resolved-ref: "41c4f32eb0eb82540a42fae25a10ece2557d517c" + ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" + resolved-ref: "819b21164ef93cc0889049d4a8a1be2d0cc36a1b" url: "https://github.com/Cyrix126/namecoin_dart" source: git - version: "0.3.0" + version: "2.0.0" nanodart: dependency: "direct main" description: diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 2e83d017d..145e191ed 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -217,7 +217,7 @@ dependencies: namecoin: git: url: https://github.com/Cyrix126/namecoin_dart - ref: 41c4f32eb0eb82540a42fae25a10ece2557d517c + ref: 819b21164ef93cc0889049d4a8a1be2d0cc36a1b dev_dependencies: flutter_test: From 4425e55596f96976e11103efac3d86b7be4abc38 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 12:45:46 -0600 Subject: [PATCH 04/29] use private key for deterministic salt --- .../namecoin_names_home_view.dart | 12 ++++++++++-- lib/wallets/wallet/impl/namecoin_wallet.dart | 17 ++++++++++++++--- .../wallet/intermediate/bip39_hd_wallet.dart | 13 +++++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart index 531d543a3..2b2a444e4 100644 --- a/lib/pages/namecoin_names/namecoin_names_home_view.dart +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -54,7 +54,15 @@ class _NamecoinNamesHomeViewState extends ConsumerState { ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; Future _preRegister() async { - final data = scriptNameNew(lastAvailableName!); + final myAddress = await _wallet.getCurrentReceivingAddress(); + if (myAddress == null) { + throw Exception("No receiving address found"); + } + + // get address private key for deterministic salt + final pk = await _wallet.getPrivateKey(myAddress); + + final data = scriptNameNew(lastAvailableName!, pk.data); // TODO: fill out properly TxData txData = TxData( @@ -69,7 +77,7 @@ class _NamecoinNamesHomeViewState extends ConsumerState { feeRateType: FeeRateType.slow, // TODO: make configurable? recipients: [ ( - address: (await _wallet.getCurrentReceivingAddress())!.value, + address: myAddress.value, isChange: false, amount: Amount.fromDecimal( Decimal.parse("0.015"), diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index a911215bb..8bc9581a7 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -497,9 +497,20 @@ class NamecoinWallet final sKey = nameSaltKeyBuilder(utxo.txid, walletId); - final data = decodeNameSaltData( - (await secureStorageInterface.read(key: sKey))!, - ); + final encoded = await secureStorageInterface.read(key: sKey); + if (encoded == null) { + continue; + } + + final data = decodeNameSaltData(encoded); + + // verify cached matches + final myAddress = await mainDB.getAddress(walletId, utxo.address!); + final pk = await getPrivateKey(myAddress!); + final generatedSalt = scriptNameNew(data.name, pk.data).$2; + + // TODO replace assert with proper error + assert(generatedSalt == data.salt); final nameScriptHex = scriptNameFirstUpdate( data.name, diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 832ce3e13..90cbacea4 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -33,14 +33,19 @@ abstract class Bip39HDWallet extends Bip39Wallet return coinlib.HDPrivateKey.fromSeed(seed); } + Future getPrivateKey(Address address) async { + return (await getRootHDNode()) + .derivePath(address.derivationPath!.value) + .privateKey; + } + Future getPrivateKeyWIF(Address address) async { - final keys = - (await getRootHDNode()).derivePath(address.derivationPath!.value); + final privateKey = await getPrivateKey(address); final List data = [ cryptoCurrency.networkParams.wifPrefix, - ...keys.privateKey.data, - if (keys.privateKey.compressed) 1, + ...privateKey.data, + if (privateKey.compressed) 1, ]; final checksum = coinlib.sha256DoubleHash(Uint8List.fromList(data)).sublist(0, 4); From 406a9c24a18c142566f4fc5558d8e19419617047 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 12:55:07 -0600 Subject: [PATCH 05/29] auto focus cursor in search field on desktop --- .../add_wallet_views/add_wallet_view/add_wallet_view.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index 03d289237..748f5074e 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -153,7 +153,12 @@ class _AddWalletViewState extends ConsumerState { } WidgetsBinding.instance.addPostFrameCallback((_) { - ref.refresh(addWalletSelectedEntityStateProvider); + if (mounted) { + ref.refresh(addWalletSelectedEntityStateProvider); + if (isDesktop) { + _searchFocusNode.requestFocus(); + } + } }); super.initState(); From 93f7433ff9573b2f35999ba479f2ee915e6901cc Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 12:55:43 -0600 Subject: [PATCH 06/29] consistent log level edit --- lib/wallets/wallet/impl/firo_wallet.dart | 2 +- .../wallet/wallet_mixin_interfaces/electrumx_interface.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/wallets/wallet/impl/firo_wallet.dart b/lib/wallets/wallet/impl/firo_wallet.dart index 9aa1357f0..aed72a33a 100644 --- a/lib/wallets/wallet/impl/firo_wallet.dart +++ b/lib/wallets/wallet/impl/firo_wallet.dart @@ -738,7 +738,7 @@ class FiroWallet extends Bip39HDWallet ); // receiving addresses - Logging.instance.d("checking receiving addresses..."); + Logging.instance.i("checking receiving addresses..."); final canBatch = await serverCanBatch; diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 4416b918a..2660d60ff 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -1472,7 +1472,7 @@ mixin ElectrumXInterface } // receiving addresses - Logging.instance.e( + Logging.instance.i( "checking receiving addresses...", ); @@ -2003,7 +2003,7 @@ mixin ElectrumXInterface if (root != null) { // receiving addresses - Logging.instance.d( + Logging.instance.i( "checking receiving addresses...", ); From 1df42267a9c3f7a67ed2ea1e67596020d9d6f5f4 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 14:02:55 -0600 Subject: [PATCH 07/29] hack in name op utxo confirmation check --- .../isar/models/blockchain_data/utxo.dart | 9 +++- lib/pages/coin_control/coin_control_view.dart | 49 ++++++++++++++---- lib/pages/coin_control/utxo_card.dart | 34 +++++++++---- lib/pages/coin_control/utxo_details_view.dart | 28 +++++++--- .../coin_control/utxo_row.dart | 27 +++++----- lib/wallets/wallet/impl/namecoin_wallet.dart | 51 ++++++++++++++++--- 6 files changed, 148 insertions(+), 50 deletions(-) diff --git a/lib/models/isar/models/blockchain_data/utxo.dart b/lib/models/isar/models/blockchain_data/utxo.dart index e4f91258c..57f9ede64 100644 --- a/lib/models/isar/models/blockchain_data/utxo.dart +++ b/lib/models/isar/models/blockchain_data/utxo.dart @@ -85,9 +85,14 @@ class UTXO { bool isConfirmed( int currentChainHeight, int minimumConfirms, - int minimumCoinbaseConfirms, - ) { + int minimumCoinbaseConfirms, { + int? overrideMinConfirms, // added to handle namecoin name op outputs + }) { final confirmations = getConfirmations(currentChainHeight); + + if (overrideMinConfirms != null) { + return confirmations >= overrideMinConfirms; + } return confirmations >= (isCoinbase ? minimumCoinbaseConfirms : minimumConfirms); } diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 07703a149..cb288f845 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -26,6 +26,8 @@ import '../../utilities/assets.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../wallets/wallet/wallet_mixin_interfaces/coin_control_interface.dart'; import '../../widgets/animated_widgets/rotate_icon.dart'; import '../../widgets/app_bar_field.dart'; @@ -88,6 +90,18 @@ class _CoinControlViewState extends ConsumerState { await coinControlInterface.updateBalance(); } + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { if (widget.selectedUTXOs != null) { @@ -347,10 +361,15 @@ class _CoinControlViewState extends ConsumerState { CoinControlViewType.manage || (widget.type == CoinControlViewType.use && !utxo.isBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s + .getWallet(widget.walletId), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { @@ -412,10 +431,16 @@ class _CoinControlViewState extends ConsumerState { (widget.type == CoinControlViewType.use && !_showBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { @@ -557,10 +582,16 @@ class _CoinControlViewState extends ConsumerState { CoinControlViewType .use && !utxo.isBlocked && - utxo.isConfirmed( + _isConfirmed( + utxo, currentHeight, - minConfirms, - coin.minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), )), initialSelectedState: isSelected, onSelectedChanged: (value) { diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 0881c7eb6..624b41eee 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -20,6 +20,8 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; @@ -52,6 +54,18 @@ class _UtxoCardState extends ConsumerState { late bool _selected; + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { _selected = widget.initialSelectedState; @@ -110,18 +124,16 @@ class _UtxoCardState extends ConsumerState { ), child: UTXOStatusIcon( blocked: utxo.isBlocked, - status: utxo.isConfirmed( + status: _isConfirmed( + utxo, currentHeight, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), ) ? UTXOStatusIconStatus.confirmed : UTXOStatusIconStatus.unconfirmed, diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index ba71f7a01..9959abdda 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -23,6 +23,8 @@ import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../wallets/wallet/wallet.dart'; import '../../widgets/background.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; @@ -67,6 +69,18 @@ class _UtxoDetailsViewState extends ConsumerState { await MainDB.instance.putUTXO(utxo!.copyWith(isBlocked: !utxo!.isBlocked)); } + bool _isConfirmed(UTXO utxo, int currentChainHeight, Wallet wallet) { + if (wallet is NamecoinWallet) { + return wallet.checkUtxoConfirmed(utxo, currentChainHeight); + } else { + return utxo.isConfirmed( + currentChainHeight, + wallet.cryptoCurrency.minConfirms, + wallet.cryptoCurrency.minCoinbaseConfirms, + ); + } + } + @override void initState() { utxo = MainDB.instance.isar.utxos @@ -95,14 +109,14 @@ class _UtxoDetailsViewState extends ConsumerState { final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); - final confirmed = utxo!.isConfirmed( + final confirmed = _isConfirmed( + utxo!, currentHeight, - ref.watch(pWallets).getWallet(widget.walletId).cryptoCurrency.minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, + ref.watch( + pWallets.select( + (s) => s.getWallet(widget.walletId), + ), + ), ); return ConditionalParent( diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index 26204375c..548e2c05e 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -20,7 +20,9 @@ import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; import '../../utilities/text_styles.dart'; +import '../../wallets/crypto_currency/coins/namecoin.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/desktop/secondary_button.dart'; @@ -135,19 +137,18 @@ class _UtxoRowState extends ConsumerState { ), UTXOStatusIcon( blocked: utxo.isBlocked, - status: utxo.isConfirmed( - ref.watch(pWalletChainHeight(widget.walletId)), - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms, - ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minCoinbaseConfirms, - ) + status: (coin is Namecoin + ? (ref.watch(pWallets).getWallet(widget.walletId) + as NamecoinWallet) + .checkUtxoConfirmed( + utxo, + ref.watch(pWalletChainHeight(widget.walletId)), + ) + : utxo.isConfirmed( + ref.watch(pWalletChainHeight(widget.walletId)), + coin.minConfirms, + coin.minCoinbaseConfirms, + )) ? UTXOStatusIconStatus.confirmed : UTXOStatusIconStatus.unconfirmed, background: Theme.of(context).extension()!.popupBG, diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 8bc9581a7..52d6f314f 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -476,14 +476,8 @@ class NamecoinWallet .findAll(); for (final utxo in utxos) { - final otherData = jsonDecode(utxo.otherData!) as Map; - if (otherData[UTXOOtherDataKeys.nameOpData] != null) { - final nameOp = OpNameData( - (jsonDecode(otherData[UTXOOtherDataKeys.nameOpData] as String) as Map) - .cast(), - utxo.blockHeight!, - ); - + final nameOp = getOpNameDataFrom(utxo); + if (nameOp != null) { Logging.instance.t( "Found OpName: $nameOp", stackTrace: StackTrace.current, @@ -499,6 +493,9 @@ class NamecoinWallet final encoded = await secureStorageInterface.read(key: sKey); if (encoded == null) { + Logging.instance.w( + "Found OpName encoded value not found!!", + ); continue; } @@ -1202,4 +1199,42 @@ class NamecoinWallet return txData; } + + /// return null if utxo does not contain name op + OpNameData? getOpNameDataFrom(UTXO utxo) { + if (utxo.otherData == null) { + return null; + } + final otherData = jsonDecode(utxo.otherData!) as Map; + if (otherData[UTXOOtherDataKeys.nameOpData] != null) { + try { + final nameOp = OpNameData( + (jsonDecode(otherData[UTXOOtherDataKeys.nameOpData] as String) as Map) + .cast(), + utxo.blockHeight!, + ); + return nameOp; + } catch (e, s) { + Logging.instance.d( + "getOpNameDataFrom($utxo) failed", + error: e, + stackTrace: s, + ); + return null; + } + } + return null; + } + + bool checkUtxoConfirmed(UTXO utxo, int currentChainHeight) { + final isNameOpOutput = getOpNameDataFrom(utxo) != null; + + final confirmedStatus = utxo.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + overrideMinConfirms: isNameOpOutput ? kNameWaitBlocks : null, + ); + return confirmedStatus; + } } From 9072a13596553b46b99e8b50073d9b8707b04c7e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 14:07:38 -0600 Subject: [PATCH 08/29] handle name op output selection based on name op type --- lib/wallets/wallet/impl/namecoin_wallet.dart | 102 +++++++++++++++++-- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 52d6f314f..cde50427b 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -917,19 +917,103 @@ class NamecoinWallet final canCPFP = this is CpfpInterface && coinControl; - final spendableOutputs = availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || + int nameOutputCount = 0; // for sanity check. Should only be max 1; + void nameOutputCountCheck() { + nameOutputCount++; + if (nameOutputCount > 1) { + throw Exception("nameOutputCount greater than one"); + } + } + + final List spendableOutputs; + switch (txData.opNameState!.type) { + case OpName.nameNew: + spendableOutputs = availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); + break; + + case OpName.nameFirstUpdate: + spendableOutputs = availableOutputs.where( + (e) { + if (e.used == true) return false; + + final nameOp = getOpNameDataFrom(e); + if (nameOp != null) { + if (nameOp.op == OpName.nameFirstUpdate || + nameOp.op == OpName.nameUpdate) { + return false; + } else { + final confirmed = e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + overrideMinConfirms: kNameWaitBlocks, + ); + + if (confirmed) { + nameOutputCountCheck(); + } + return confirmed; + } + } else { + return canCPFP || e.isConfirmed( currentChainHeight, cryptoCurrency.minConfirms, cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); + ); + } + }, + ).toList(); + break; + + case OpName.nameUpdate: + spendableOutputs = availableOutputs.where( + (e) { + if (e.used == true) return false; + + final nameOp = getOpNameDataFrom(e); + if (nameOp != null) { + if (nameOp.op == OpName.nameFirstUpdate || + nameOp.op == OpName.nameUpdate) { + final confirmed = e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + overrideMinConfirms: kNameWaitBlocks, + ); + + if (confirmed) { + nameOutputCountCheck(); + } + return confirmed; + } else { + return false; + } + } else { + return canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + ); + } + }, + ).toList(); + break; + } + final spendableSatoshiValue = spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); From f864bb5d65923273ee58d9a5933ce5495448cad6 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 17:23:14 -0600 Subject: [PATCH 09/29] refactor auto register NAME NEW process --- .../confirm_name_transaction_view.dart | 6 +- .../namecoin_names_home_view.dart | 3 +- lib/wallets/models/name_op_state.dart | 11 +- lib/wallets/wallet/impl/namecoin_wallet.dart | 207 +++++++++++------- lib/wallets/wallet/wallet.dart | 2 +- 5 files changed, 138 insertions(+), 91 deletions(-) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index f46fcf795..df1245da2 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -126,7 +126,11 @@ class _ConfirmNameTransactionViewState // associated name data for reg tx ref.read(secureStoreProvider).write( - key: nameSaltKeyBuilder(txData.txid!, walletId), + key: nameSaltKeyBuilder( + txData.txid!, + walletId, + txData.opNameState!.outputPosition, + ), value: encodeNameSaltData( txData.opNameState!.name, txData.opNameState!.saltHex, diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart index 2b2a444e4..f52e55ec7 100644 --- a/lib/pages/namecoin_names/namecoin_names_home_view.dart +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -73,6 +73,7 @@ class _NamecoinNamesHomeViewState extends ConsumerState { value: "test", // TODO: get from user for automatic reg later nameScriptHex: data.$1, type: OpName.nameNew, + outputPosition: -1, //currently unknown, updated later ), feeRateType: FeeRateType.slow, // TODO: make configurable? recipients: [ @@ -89,8 +90,6 @@ class _NamecoinNamesHomeViewState extends ConsumerState { txData = await _wallet.prepareNameSend(txData: txData); - Logging.instance.f("SALTY: ${txData.opNameState!.saltHex}"); - if (mounted) { if (Util.isDesktop) { await showDialog( diff --git a/lib/wallets/models/name_op_state.dart b/lib/wallets/models/name_op_state.dart index 98ca00e2d..577663f12 100644 --- a/lib/wallets/models/name_op_state.dart +++ b/lib/wallets/models/name_op_state.dart @@ -2,13 +2,12 @@ import 'package:namecoin/namecoin.dart'; class NameOpState { final String name; - final OpName type; - final String saltHex; final String commitment; final String value; final String nameScriptHex; + final int outputPosition; NameOpState({ required this.name, @@ -17,17 +16,17 @@ class NameOpState { required this.commitment, required this.value, required this.nameScriptHex, + required this.outputPosition, }); NameOpState copyWith({ - String? walletId, String? name, - String? txid, OpName? type, String? saltHex, String? commitment, String? value, String? nameScriptHex, + int? outputPosition, }) { return NameOpState( name: name ?? this.name, @@ -36,6 +35,7 @@ class NameOpState { commitment: commitment ?? this.commitment, value: value ?? this.value, nameScriptHex: nameScriptHex ?? this.nameScriptHex, + outputPosition: outputPosition ?? this.outputPosition, ); } @@ -47,6 +47,7 @@ class NameOpState { "saltHex: $saltHex, " "commitment: $commitment, " "value: $value, " - "nameScriptHex: $nameScriptHex)"; + "nameScriptHex: $nameScriptHex, " + "outputPosition: $outputPosition)"; } } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index cde50427b..6627449c4 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -31,8 +31,13 @@ const kNameTxVersion = 0x7100; const _kNameSaltSplitter = r"$$$$"; -String nameSaltKeyBuilder(String txid, String walletId) => - "${walletId}_${txid}_nameSaltData"; +String nameSaltKeyBuilder(String txid, String walletId, int txPos) { + if (txPos.isNegative) { + throw Exception("Invalid vout index"); + } + + return "${walletId}_${txid}_${txPos}nameSaltData"; +} String encodeNameSaltData(String name, String salt, String value) => "$name$_kNameSaltSplitter$salt$_kNameSaltSplitter$value"; @@ -461,99 +466,118 @@ class NamecoinWallet return (data: opNameData, available: available); } + // TODO: handle this differently? + final Set<(int, String)> _unknownNameNewOutputs = {}; + /// Must be called in refresh() AFTER the wallet's UTXOs have been updated! - Future checkForNameNewOPs() async { - final currentHeight = await chainHeight; - // not ideal filtering - final utxos = await mainDB - .getUTXOs(walletId) - .filter() - .otherDataIsNotNull() - .and() - .blockHeightIsNotNull() - .and() - .blockHeightLessThan(currentHeight - kNameWaitBlocks) - .findAll(); + Future checkAutoRegisterNameNewOutputs() async { + try { + final currentHeight = await chainHeight; + // not ideal filtering + final utxos = await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .and() + .blockHeightIsNotNull() + .and() + .blockHeightGreaterThan(0) + .and() + .blockHeightLessThan(currentHeight - kNameWaitBlocks) + .findAll(); + + Logging.instance.t( + "_unknownNameNewOutputs(count=${_unknownNameNewOutputs.length})" + ":\n$_unknownNameNewOutputs", + ); - for (final utxo in utxos) { - final nameOp = getOpNameDataFrom(utxo); - if (nameOp != null) { - Logging.instance.t( - "Found OpName: $nameOp", - stackTrace: StackTrace.current, - ); + // check cache and remove known auto unspendable name new outputs + utxos.removeWhere( + (e) => _unknownNameNewOutputs.contains((e.vout, e.txid))); + + for (final utxo in utxos) { + final nameOp = getOpNameDataFrom(utxo); + if (nameOp != null) { + Logging.instance.t( + "Found OpName: $nameOp\n\nIN UTXO: $utxo", + ); - if (nameOp.op == OpName.nameNew) { - // at this point we should have an unspent UTXO that is at least - // 12 blocks old which we can now do nameFirstUpdate on + if (nameOp.op == OpName.nameNew) { + // at this point we should have an unspent UTXO that is at least + // 12 blocks old which we can now do nameFirstUpdate on - //TODO: Should check if name was registered by someone else here + //TODO: Should check if name was registered by someone else here - final sKey = nameSaltKeyBuilder(utxo.txid, walletId); + final sKey = nameSaltKeyBuilder(utxo.txid, walletId, utxo.vout); - final encoded = await secureStorageInterface.read(key: sKey); - if (encoded == null) { - Logging.instance.w( - "Found OpName encoded value not found!!", - ); - continue; - } + final encoded = await secureStorageInterface.read(key: sKey); + if (encoded == null) { + Logging.instance.d( + "Found OpName NAME NEW utxo without local matching data." + "\nUTXO: $utxo" + "\nUnable to auto register.", + ); + _unknownNameNewOutputs.add((utxo.vout, utxo.txid)); + continue; + } - final data = decodeNameSaltData(encoded); + final data = decodeNameSaltData(encoded); - // verify cached matches - final myAddress = await mainDB.getAddress(walletId, utxo.address!); - final pk = await getPrivateKey(myAddress!); - final generatedSalt = scriptNameNew(data.name, pk.data).$2; + // verify cached matches + final myAddress = await mainDB.getAddress(walletId, utxo.address!); + final pk = await getPrivateKey(myAddress!); + final generatedSalt = scriptNameNew(data.name, pk.data).$2; - // TODO replace assert with proper error - assert(generatedSalt == data.salt); + // TODO replace assert with proper error + assert(generatedSalt == data.salt); - final nameScriptHex = scriptNameFirstUpdate( - data.name, - data.value, - data.salt, - ); + final nameScriptHex = scriptNameFirstUpdate( + data.name, + data.value, + data.salt, + ); - TxData txData = TxData( - opNameState: NameOpState( - name: data.name, - saltHex: data.salt, - commitment: "n/a", - value: data.value, - nameScriptHex: nameScriptHex, - type: OpName.nameFirstUpdate, - ), - feeRateType: FeeRateType.slow, // TODO: make configurable? - recipients: [ - ( - address: (await getCurrentReceivingAddress())!.value, - isChange: false, - amount: Amount.fromDecimal( - Decimal.parse("0.01"), - fractionDigits: cryptoCurrency.fractionDigits, - ), + TxData txData = TxData( + utxos: {utxo}, + opNameState: NameOpState( + name: data.name, + saltHex: data.salt, + commitment: "n/a", + value: data.value, + nameScriptHex: nameScriptHex, + type: OpName.nameFirstUpdate, + outputPosition: -1, //currently unknown, updated later ), - ], - ); + feeRateType: FeeRateType.slow, // TODO: make configurable? + recipients: [ + ( + address: (await getCurrentReceivingAddress())!.value, + isChange: false, + amount: Amount.fromDecimal( + Decimal.parse("0.01"), + fractionDigits: cryptoCurrency.fractionDigits, + ), + ), + ], + ); - txData = await prepareNameSend(txData: txData); + // generate tx + txData = await prepareNameSend(txData: txData); - txData = await confirmSend(txData: txData); + // broadcast tx + txData = await confirmSend(txData: txData); - // TODO - await secureStorageInterface.delete(key: sKey); - // TODO - await secureStorageInterface.write( - key: nameSaltKeyBuilder(txData.txid!, walletId), - value: encodeNameSaltData( - txData.opNameState!.name, - txData.opNameState!.saltHex, - txData.opNameState!.value, - ), - ); + // clear out value from local secure storage on successful registration + await secureStorageInterface.delete(key: sKey); + } } } + } catch (e, s) { + Logging.instance.e( + "checkAutoRegisterNameNewOutputs() failed", + error: e, + stackTrace: s, + ); } } @@ -603,7 +627,7 @@ class NamecoinWallet : 0xffffffff - 1; // Add transaction inputs - for (var i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < utxoSigningData.length; i++) { final txid = utxoSigningData[i].utxo.txid; final hash = Uint8List.fromList( @@ -685,9 +709,11 @@ class NamecoinWallet ); } + int? nameOpVoutIndex; + int nonChangeCount = 0; // sanity check counter. Should only hit 1. // Add transaction outputs - for (var i = 0; i < txData.recipients!.length; i++) { + for (int i = 0; i < txData.recipients!.length; i++) { final address = coinlib.Address.fromString( normalizeAddress(txData.recipients![i].address), cryptoCurrency.networkParams, @@ -709,7 +735,13 @@ class NamecoinWallet txData.opNameState!.nameScriptHex.toUint8ListFromHex + scriptPubKey, ), ); + // redundant sanity check + if (nameOpVoutIndex != null) { + throw Exception("More than one NAME OP output detected!"); + } + nameOpVoutIndex = i; } else { + // change output output = coinlib.Output.fromAddress( txData.recipients![i].amount.raw, address, @@ -739,7 +771,7 @@ class NamecoinWallet try { // Sign the transaction accordingly - for (var i = 0; i < utxoSigningData.length; i++) { + for (int i = 0; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; @@ -767,9 +799,16 @@ class NamecoinWallet rethrow; } + if (nameOpVoutIndex == null) { + throw Exception("No NAME OP output detected!"); + } + return txData.copyWith( raw: clTx.toHex(), vSize: clTx.vSize(), + opNameState: txData.opNameState!.copyWith( + outputPosition: nameOpVoutIndex, + ), tempTx: TransactionV2( walletId: walletId, blockHash: null, @@ -799,6 +838,10 @@ class NamecoinWallet throw Exception("No recipients in attempted transaction!"); } + Logging.instance.t( + "prepareNameSend called with TxData:\n\n$txData", + ); + final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; final feeRateAmount = txData.feeRateAmount; @@ -1049,7 +1092,7 @@ class NamecoinWallet final List utxoObjectsToUse = []; if (!coinControl) { - for (var i = 0; + for (int i = 0; satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; i++) { diff --git a/lib/wallets/wallet/wallet.dart b/lib/wallets/wallet/wallet.dart index 9f74fbcee..161c68f5f 100644 --- a/lib/wallets/wallet/wallet.dart +++ b/lib/wallets/wallet/wallet.dart @@ -666,7 +666,7 @@ abstract class Wallet { if (this is NamecoinWallet) { await updateUTXOs(); _fireRefreshPercentChange(0.6); - await (this as NamecoinWallet).checkForNameNewOPs(); + await (this as NamecoinWallet).checkAutoRegisterNameNewOutputs(); _fireRefreshPercentChange(0.70); await updateTransactions(); } else { From a4ed1094a2bd9d5df1a058faa6032b737bb47f65 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 17:36:09 -0600 Subject: [PATCH 10/29] update namecoin dns More option name and icon --- lib/pages/wallet_view/wallet_view.dart | 4 ++-- .../sub_widgets/more_features/more_features_dialog.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index a4a846dcf..41422c7e0 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1176,8 +1176,8 @@ class _WalletViewState extends ConsumerState { ), if (wallet is NamecoinWallet) WalletNavigationBarItemData( - label: "Names", - icon: const CoinControlNavIcon(), + label: "Domains", + icon: const PaynymNavIcon(), onTap: () { Navigator.of(context).pushNamed( NamecoinNamesHomeView.routeName, 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 72a5d0778..ef452c09b 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 @@ -479,9 +479,9 @@ class _MoreFeaturesDialogState extends ConsumerState { ), if (wallet is NamecoinWallet) _MoreFeaturesItem( - label: "Names", + label: "Domains", detail: "Namecoin DNS", - iconAsset: Assets.svg.file, + iconAsset: Assets.svg.robotHead, onPressed: () async => widget.onNamesPressed?.call(), ), if (wallet is SparkInterface && !isViewOnly) From 124c8478348d64c298808cfce48e7a9e86a6cc14 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 13 Feb 2025 23:00:06 -0600 Subject: [PATCH 11/29] WIP names gui --- .../namecoin_names_home_view.dart | 289 +++----------- .../sub_widgets/buy_domain_option_widget.dart | 362 ++++++++++++++++++ .../manage_domains_option_widget.dart | 55 +++ lib/wallets/wallet/impl/namecoin_wallet.dart | 68 +++- 4 files changed, 522 insertions(+), 252 deletions(-) create mode 100644 lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart create mode 100644 lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart index f52e55ec7..8cc1e5068 100644 --- a/lib/pages/namecoin_names/namecoin_names_home_view.dart +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -1,36 +1,19 @@ -import 'dart:async'; - -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:isar/isar.dart'; -import 'package:namecoin/namecoin.dart'; -import '../../models/isar/models/blockchain_data/utxo.dart'; -import '../../providers/db/main_db_provider.dart'; -import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; -import '../../utilities/amount/amount.dart'; import '../../utilities/assets.dart'; -import '../../utilities/enums/fee_rate_type_enum.dart'; -import '../../utilities/logger.dart'; -import '../../utilities/show_loading.dart'; +import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; import '../../utilities/util.dart'; -import '../../wallets/models/name_op_state.dart'; -import '../../wallets/models/tx_data.dart'; -import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/desktop/desktop_app_bar.dart'; -import '../../widgets/desktop/desktop_dialog.dart'; import '../../widgets/desktop/desktop_scaffold.dart'; -import '../../widgets/desktop/primary_button.dart'; -import '../../widgets/desktop/secondary_button.dart'; -import '../../widgets/rounded_white_container.dart'; -import '../../widgets/stack_dialog.dart'; -import 'confirm_name_transaction_view.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/buy_domain_option_widget.dart'; +import 'sub_widgets/manage_domains_option_widget.dart'; class NamecoinNamesHomeView extends ConsumerStatefulWidget { const NamecoinNamesHomeView({ @@ -48,69 +31,7 @@ class NamecoinNamesHomeView extends ConsumerStatefulWidget { } class _NamecoinNamesHomeViewState extends ConsumerState { - String? lastAvailableName; - - NamecoinWallet get _wallet => - ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; - - Future _preRegister() async { - final myAddress = await _wallet.getCurrentReceivingAddress(); - if (myAddress == null) { - throw Exception("No receiving address found"); - } - - // get address private key for deterministic salt - final pk = await _wallet.getPrivateKey(myAddress); - - final data = scriptNameNew(lastAvailableName!, pk.data); - - // TODO: fill out properly - TxData txData = TxData( - opNameState: NameOpState( - name: lastAvailableName!, - saltHex: data.$2, - commitment: data.$3, - value: "test", // TODO: get from user for automatic reg later - nameScriptHex: data.$1, - type: OpName.nameNew, - outputPosition: -1, //currently unknown, updated later - ), - feeRateType: FeeRateType.slow, // TODO: make configurable? - recipients: [ - ( - address: myAddress.value, - isChange: false, - amount: Amount.fromDecimal( - Decimal.parse("0.015"), - fractionDigits: _wallet.cryptoCurrency.fractionDigits, - ), - ), - ], - ); - - txData = await _wallet.prepareNameSend(txData: txData); - - if (mounted) { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: _wallet.walletId, - ), - ), - ); - } else { - await Navigator.of(context).pushNamed( - ConfirmNameTransactionView.routeName, - arguments: (txData, _wallet.walletId), - ); - } - } - } + bool _onManage = true; @override Widget build(BuildContext context) { @@ -148,7 +69,7 @@ class _NamecoinNamesHomeViewState extends ConsumerState { ), ), SvgPicture.asset( - Assets.svg.file, + Assets.svg.robotHead, width: 32, height: 32, color: Theme.of(context).extension()!.textDark, @@ -157,7 +78,7 @@ class _NamecoinNamesHomeViewState extends ConsumerState { width: 10, ), Text( - "Names", + "Domains", style: STextStyles.desktopH3(context), ), ], @@ -171,7 +92,7 @@ class _NamecoinNamesHomeViewState extends ConsumerState { ), titleSpacing: 0, title: Text( - "Names", + "Domains", style: STextStyles.navBarTitle(context), overflow: TextOverflow.ellipsis, ), @@ -188,52 +109,51 @@ class _NamecoinNamesHomeViewState extends ConsumerState { crossAxisAlignment: isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ - LookupNameForm( - walletId: widget.walletId, - onNameAvailable: (name) { - if (name != lastAvailableName) { - setState(() { - lastAvailableName = name; - }); - } - }, - ), - if (lastAvailableName != null) - PrimaryButton( - label: "Register $lastAvailableName", - onPressed: _preRegister, + Padding( + padding: EdgeInsets.only( + top: Util.isDesktop ? 24 : 16, + left: Util.isDesktop ? 24 : 16, + right: Util.isDesktop ? 24 : 16, ), - const SizedBox( - height: 32, - ), - Expanded( - child: StreamBuilder( - stream: ref.watch( - mainDBProvider.select( - (s) => s.isar.utxos - .where() - .walletIdEqualTo(widget.walletId) - .filter() - .otherDataIsNotNull() - .watch(fireImmediately: true), + child: SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Buy domain", + offText: "Manage domains", + isOn: !_onManage, + onValueChanged: (value) { + setState(() { + _onManage = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), ), - builder: (context, snapshot) { - List list = []; - if (snapshot.hasData) { - list = snapshot.data!; - } - - return ListView.separated( - itemCount: list.length, - itemBuilder: (context, index) => RoundedWhiteContainer( - child: Text(list[index].otherData!), + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), + child: IndexedStack( + index: _onManage ? 0 : 1, + children: [ + BuyDomainOptionWidget( + walletId: widget.walletId, ), - separatorBuilder: (context, index) => const SizedBox( - height: 10, + ManageDomainsOptionWidget( + walletId: widget.walletId, ), - ); - }, + ], + ), ), ), ], @@ -242,116 +162,3 @@ class _NamecoinNamesHomeViewState extends ConsumerState { ); } } - -class LookupNameForm extends ConsumerStatefulWidget { - const LookupNameForm({ - super.key, - required this.walletId, - this.onNameAvailable, - }); - - final String walletId; - - final void Function(String? name)? onNameAvailable; - - @override - ConsumerState createState() => _LookupNameFormState(); -} - -class _LookupNameFormState extends ConsumerState { - final nameController = TextEditingController(); - final nameFieldFocus = FocusNode(); - - NamecoinWallet get _wallet => - ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; - - bool _lookupLock = false; - Future _lookup() async { - if (_lookupLock) return; - _lookupLock = true; - try { - widget.onNameAvailable?.call(null); - final result = await showLoading( - whileFuture: _wallet.lookupName(nameController.text), - context: context, - message: "Looking up ${nameController.text}", - onException: (e) => throw e, - rootNavigator: Util.isDesktop, - delay: const Duration(seconds: 2), - ); - - if (result?.available == true) { - widget.onNameAvailable?.call(nameController.text); - } - - Logging.instance.i("LOOKUP RESULT: $result"); - } catch (e, s) { - widget.onNameAvailable?.call(null); - Logging.instance.e("_lookup failed", error: e, stackTrace: s); - - if (mounted) { - await showDialog( - context: context, - builder: (_) => StackOkDialog( - title: "Name lookup failed", - desktopPopRootNavigator: Util.isDesktop, - maxWidth: Util.isDesktop ? 600 : null, - ), - ); - } - } finally { - _lookupLock = false; - } - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - nameFieldFocus.requestFocus(); - } - }); - } - - @override - void dispose() { - nameController.dispose(); - nameFieldFocus.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: - Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, - children: [ - TextField( - textInputAction: TextInputAction.search, - focusNode: nameFieldFocus, - controller: nameController, - onSubmitted: (_) { - if (nameController.text.isNotEmpty) { - _lookup(); - } - }, - onChanged: (_) { - // trigger look up button enabled/disabled state change - setState(() {}); - }, - ), - const SizedBox( - height: 20, - ), - SecondaryButton( - label: "Look up name", - enabled: nameController.text.isNotEmpty, - width: 160, - buttonHeight: ButtonHeight.l, - onPressed: _lookup, - ), - ], - ); - } -} diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart new file mode 100644 index 000000000..feb9b93ce --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -0,0 +1,362 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../providers/providers.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/rounded_white_container.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../confirm_name_transaction_view.dart'; + +class BuyDomainOptionWidget extends ConsumerStatefulWidget { + const BuyDomainOptionWidget({super.key, required this.walletId}); + + final String walletId; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + final _nameController = TextEditingController(); + final _nameFieldFocus = FocusNode(); + + String? get formattedNameInField { + if (_nameController.text.isNotEmpty) { + if (_nameController.text.startsWith("d/")) { + return _nameController.text; + } else { + return "d/${_nameController.text}"; + } + } + return null; + } + + bool _isAvailable = false; + String? _lastLookedUpName; + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try { + _isAvailable = false; + + _lastLookedUpName = formattedNameInField; + final result = await showLoading( + whileFuture: + (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet) + .lookupName(_lastLookedUpName!), + context: context, + message: "Searching...", + onException: (e) => throw e, + rootNavigator: Util.isDesktop, + delay: const Duration(seconds: 2), + ); + + _isAvailable = result?.nameState == NameState.available; + + if (mounted) { + setState(() {}); + } + + Logging.instance.i("LOOKUP RESULT: $result"); + } catch (e, s) { + Logging.instance.e("_lookup failed", error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Name lookup failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Container( + height: 48, + width: 100, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), // Adjust radius as needed + bottomLeft: + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + textInputAction: TextInputAction.search, + focusNode: _nameFieldFocus, + controller: _nameController, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + prefixIcon: Padding( + padding: const EdgeInsets.all(14), + child: SvgPicture.asset( + Assets.svg.search, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + fillColor: Colors.transparent, + hintText: "Find a domain name", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + onSubmitted: (_) { + if (_nameController.text.isNotEmpty) { + _lookup(); + } + }, + onChanged: (_) { + // trigger look up button enabled/disabled state change + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + Container( + height: 48, + width: 100, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .buttonBackPrimary, + borderRadius: BorderRadius.only( + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), // Adjust radius as needed + bottomRight: + Radius.circular(Constants.size.circularBorderRadius), + ), + ), + child: Center( + child: Text( + ".bit", + style: STextStyles.w600_14(context).copyWith( + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + ), + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + SecondaryButton( + label: "Lookup", + enabled: _nameController.text.isNotEmpty, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _lookup, + ), + const SizedBox( + height: 32, + ), + if (_lastLookedUpName != null) + _NameCard( + walletId: widget.walletId, + isAvailable: _isAvailable, + formattedName: _lastLookedUpName!, + ), + ], + ); + } +} + +class _NameCard extends ConsumerWidget { + const _NameCard({ + super.key, + required this.walletId, + required this.isAvailable, + required this.formattedName, + }); + + final String walletId; + final bool isAvailable; + final String formattedName; + + Future _preRegister( + BuildContext context, + NamecoinWallet wallet, + String value, + ) async { + final myAddress = await wallet.getCurrentReceivingAddress(); + if (myAddress == null) { + throw Exception("No receiving address found"); + } + + // get address private key for deterministic salt + final pk = await wallet.getPrivateKey(myAddress); + + final data = scriptNameNew(formattedName, pk.data); + + TxData txData = TxData( + opNameState: NameOpState( + name: formattedName, + saltHex: data.$2, + commitment: data.$3, + value: value, + nameScriptHex: data.$1, + type: OpName.nameNew, + outputPosition: -1, //currently unknown, updated later + ), + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: myAddress.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + txData = await wallet.prepareNameSend(txData: txData); + + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: wallet.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, wallet.walletId), + ); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final availability = isAvailable ? "Available" : "Unavailable"; + final color = isAvailable + ? Theme.of(context).extension()!.accentColorGreen + : Theme.of(context).extension()!.accentColorRed; + + final style = (Util.isDesktop + ? STextStyles.w500_16(context) + : STextStyles.w500_12(context)); + + return RoundedWhiteContainer( + padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), + child: IntrinsicHeight( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${formattedName.substring(2)}.bit", + style: style, + ), + Text( + availability, + style: style.copyWith( + color: color, + ), + ), + ], + ), + PrimaryButton( + label: "Buy domain", + enabled: isAvailable, + buttonHeight: ButtonHeight.m, + width: 140, + onPressed: () => _preRegister( + context, + ref.read(pWallets).getWallet(walletId) as NamecoinWallet, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart new file mode 100644 index 000000000..a9261c2bb --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../widgets/rounded_white_container.dart'; + +class ManageDomainsOptionWidget extends ConsumerStatefulWidget { + const ManageDomainsOptionWidget({ + super.key, + required this.walletId, + }); + + final String walletId; + + @override + ConsumerState createState() => + _ManageDomainsWidgetState(); +} + +class _ManageDomainsWidgetState + extends ConsumerState { + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: ref.watch( + mainDBProvider.select( + (s) => s.isar.utxos + .where() + .walletIdEqualTo(widget.walletId) + .filter() + .otherDataIsNotNull() + .watch(fireImmediately: true), + ), + ), + builder: (context, snapshot) { + List list = []; + if (snapshot.hasData) { + list = snapshot.data!; + } + + return ListView.separated( + itemCount: list.length, + itemBuilder: (context, index) => RoundedWhiteContainer( + child: Text(list[index].otherData!), + ), + separatorBuilder: (context, index) => const SizedBox( + height: 10, + ), + ); + }, + ); + } +} diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 6627449c4..ff7bce0cf 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib; -import 'package:decimal/decimal.dart'; import 'package:isar/isar.dart'; import 'package:namecoin/namecoin.dart'; @@ -28,6 +27,10 @@ import '../wallet_mixin_interfaces/rbf_interface.dart'; const kNameWaitBlocks = blocksMinToRenewName; const kNameTxVersion = 0x7100; +const kNameTxDefaultFeeRate = FeeRateType.slow; + +const kNameNewAmountSats = 150_0000; +const kNameAmountSats = 100_0000; const _kNameSaltSplitter = r"$$$$"; @@ -429,7 +432,38 @@ class NamecoinWallet // namecoin names ============================================================ - Future<({OpNameData? data, bool available})> lookupName(String name) async { + Future<({OpNameData? data, NameState nameState})> lookupName( + String name, + ) async { + // first check own utxos. Should only need to check NAME NEW here. + // NAME UPDATE and NAME FIRST UPDATE will appear readable from electrumx + final utxos = + await mainDB.getUTXOs(walletId).filter().otherDataIsNotNull().findAll(); + for (final utxo in utxos) { + final nameOp = getOpNameDataFrom(utxo); + if (nameOp?.op == OpName.nameNew) { + Logging.instance.f(utxo); + final sKey = nameSaltKeyBuilder(utxo.txid, walletId, utxo.vout); + + final encoded = await secureStorageInterface.read(key: sKey); + if (encoded == null) { + // seems this NAME NEW was created elsewhere + continue; + } + + final data = decodeNameSaltData(encoded); + Logging.instance.e( + data, + ); + if (data.name == name) { + return ( + data: null, + nameState: NameState.unavailable, + ); + } + } + } + bool available = false; final nameScriptHash = nameIdentifierToScriptHash(name); @@ -463,7 +497,10 @@ class NamecoinWallet available = true; } - return (data: opNameData, available: available); + return ( + data: opNameData, + nameState: available ? NameState.available : NameState.unavailable, + ); } // TODO: handle this differently? @@ -471,6 +508,9 @@ class NamecoinWallet /// Must be called in refresh() AFTER the wallet's UTXOs have been updated! Future checkAutoRegisterNameNewOutputs() async { + Logging.instance.t( + "$walletId checkAutoRegisterNameNewOutputs()", + ); try { final currentHeight = await chainHeight; // not ideal filtering @@ -493,7 +533,8 @@ class NamecoinWallet // check cache and remove known auto unspendable name new outputs utxos.removeWhere( - (e) => _unknownNameNewOutputs.contains((e.vout, e.txid))); + (e) => _unknownNameNewOutputs.contains((e.vout, e.txid)), + ); for (final utxo in utxos) { final nameOp = getOpNameDataFrom(utxo); @@ -548,13 +589,13 @@ class NamecoinWallet type: OpName.nameFirstUpdate, outputPosition: -1, //currently unknown, updated later ), - feeRateType: FeeRateType.slow, // TODO: make configurable? + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ ( address: (await getCurrentReceivingAddress())!.value, isChange: false, - amount: Amount.fromDecimal( - Decimal.parse("0.01"), + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), fractionDigits: cryptoCurrency.fractionDigits, ), ), @@ -598,12 +639,12 @@ class NamecoinWallet switch (txData.opNameState!.type) { case OpName.nameNew: assert( - nameAmount.decimal.toString() == "0.015", + nameAmount.raw == BigInt.from(kNameNewAmountSats), ); break; case OpName.nameFirstUpdate || OpName.nameUpdate: assert( - nameAmount.decimal.toString() == "0.01", + nameAmount.raw == BigInt.from(kNameAmountSats), ); break; } @@ -941,10 +982,10 @@ class NamecoinWallet final int expectedSatsValue; switch (txData.opNameState!.type) { case OpName.nameNew: - expectedSatsValue = 150_0000; + expectedSatsValue = kNameNewAmountSats; break; case OpName.nameFirstUpdate || OpName.nameUpdate: - expectedSatsValue = 100_0000; + expectedSatsValue = kNameAmountSats; break; } @@ -1365,3 +1406,8 @@ class NamecoinWallet return confirmedStatus; } } + +enum NameState { + available, + unavailable; +} From d2516272cb56e0421a5fdfa2311705f8cb29e7e3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Feb 2025 09:24:11 -0600 Subject: [PATCH 12/29] change particl default address type to old --- lib/wallets/crypto_currency/coins/particl.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallets/crypto_currency/coins/particl.dart b/lib/wallets/crypto_currency/coins/particl.dart index 067aae72d..8788b6114 100644 --- a/lib/wallets/crypto_currency/coins/particl.dart +++ b/lib/wallets/crypto_currency/coins/particl.dart @@ -219,7 +219,7 @@ class Particl extends Bip39HDCurrency with ElectrumXCurrencyInterface { int get targetBlockTimeSeconds => 600; @override - DerivePathType get defaultDerivePathType => DerivePathType.bip84; + DerivePathType get defaultDerivePathType => DerivePathType.bip44; @override Uri defaultBlockExplorer(String txid) { From dae4b59d1d9795a83b101e9ef228a347d2fb6686 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Feb 2025 16:18:03 -0600 Subject: [PATCH 13/29] WIP: namecoin domain name buy and add record ui --- .../dns_a_record_address_type.dart | 25 + lib/models/namecoin_dns/dns_record.dart | 90 ++++ lib/models/namecoin_dns/dns_record_type.dart | 45 ++ .../add_dns_record/add_dns_step_1.dart | 234 +++++++++ .../add_dns_record/add_dns_step_2.dart | 156 ++++++ .../add_dns_record/name_form_interface.dart | 67 +++ .../add_dns_record/sub_widgets/a_form.dart | 216 +++++++++ .../sub_widgets/cname_form.dart | 52 ++ .../add_dns_record/sub_widgets/ds_form.dart | 104 ++++ .../sub_widgets/import_form.dart | 69 +++ .../add_dns_record/sub_widgets/ns_form.dart | 54 +++ .../add_dns_record/sub_widgets/srv_form.dart | 104 ++++ .../add_dns_record/sub_widgets/ssh_form.dart | 88 ++++ .../add_dns_record/sub_widgets/tls_form.dart | 63 +++ .../add_dns_record/sub_widgets/txt_form.dart | 52 ++ lib/pages/namecoin_names/buy_domain_view.dart | 458 ++++++++++++++++++ .../sub_widgets/buy_domain_option_widget.dart | 122 ++--- lib/route_generator.dart | 16 + 18 files changed, 1944 insertions(+), 71 deletions(-) create mode 100644 lib/models/namecoin_dns/dns_a_record_address_type.dart create mode 100644 lib/models/namecoin_dns/dns_record.dart create mode 100644 lib/models/namecoin_dns/dns_record_type.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/name_form_interface.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart create mode 100644 lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart create mode 100644 lib/pages/namecoin_names/buy_domain_view.dart diff --git a/lib/models/namecoin_dns/dns_a_record_address_type.dart b/lib/models/namecoin_dns/dns_a_record_address_type.dart new file mode 100644 index 000000000..8709baa49 --- /dev/null +++ b/lib/models/namecoin_dns/dns_a_record_address_type.dart @@ -0,0 +1,25 @@ +enum DNSAddressType { + IPv4, + IPv6, + Tor, + Freenet, + I2P, + ZeroNet; + + String get key { + switch (this) { + case DNSAddressType.IPv4: + return "ip"; + case DNSAddressType.IPv6: + return "ip6"; + case DNSAddressType.Tor: + return "_tor"; + case DNSAddressType.Freenet: + return "freenet"; + case DNSAddressType.I2P: + return "i2p"; + case DNSAddressType.ZeroNet: + return "zeronet"; + } + } +} diff --git a/lib/models/namecoin_dns/dns_record.dart b/lib/models/namecoin_dns/dns_record.dart new file mode 100644 index 000000000..ee0842b34 --- /dev/null +++ b/lib/models/namecoin_dns/dns_record.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; + +import 'package:meta/meta.dart'; + +import '../../utilities/logger.dart'; +import 'dns_a_record_address_type.dart'; +import 'dns_record_type.dart'; + +@Immutable() +final class DNSRecord { + final DNSRecordType type; + final Map data; + + DNSRecord({ + required this.type, + required this.data, + }); + + String jsonDataString() => jsonEncode(data); + + DNSRecord copyWith({ + DNSRecordType? type, + DNSAddressType? addressType, + Map? data, + }) { + return DNSRecord( + type: type ?? this.type, + data: data ?? this.data, + ); + } + + @override + String toString() { + return "DNSRecord(type: $type, data: $data)"; + } + + static String merge(List records) { + final start = DateTime.now(); + + final Map result = {}; + + for (final record in records) { + switch (record.type) { + case DNSRecordType.CNAME: + if (result[record.data.keys.first] != null) { + throw Exception("CNAME record already exists"); + } + _deepMerge(result, record.data); + break; + + case DNSRecordType.TLS: + case DNSRecordType.NS: + case DNSRecordType.DS: + case DNSRecordType.SRV: + case DNSRecordType.SSH: + case DNSRecordType.TXT: + case DNSRecordType.IMPORT: + case DNSRecordType.A: + _deepMerge(result, record.data); + break; + } + } + + Logging.instance.w(DateTime.now().difference(start)); + return jsonEncode(result); + } +} + +void _deepMerge(Map base, Map updates) { + updates.forEach((key, value) { + if (value is Map && base[key] is Map) { + _deepMerge(base[key] as Map, value); + } else if (value is List && base[key] is List) { + (base[key] as List).addAll(value); + } else { + if (base[key] != null) { + throw Exception( + "Attempted to overwrite value: ${base[key]} where key=$key", + ); + } + if (value is Map) { + base[key] = Map.from(value); + } else if (value is List) { + base[key] = List.from(value); + } else { + base[key] = value; + } + } + }); +} diff --git a/lib/models/namecoin_dns/dns_record_type.dart b/lib/models/namecoin_dns/dns_record_type.dart new file mode 100644 index 000000000..c8e215194 --- /dev/null +++ b/lib/models/namecoin_dns/dns_record_type.dart @@ -0,0 +1,45 @@ +enum DNSRecordType { + A, + CNAME, + NS, + DS, + TLS, + SRV, + TXT, + IMPORT, + SSH; + + String get info { + switch (this) { + case DNSRecordType.A: + return "An A record maps your domain to an address (IPv4, IPv6, Tor," + " Freenet, I2P, or ZeroNet)."; + case DNSRecordType.CNAME: + return "A CNAME record redirects your domain to another domain," + " essentially acting as an alias."; + case DNSRecordType.NS: + return "An NS record specifies the nameservers that are authoritative" + " for your domain."; + case DNSRecordType.DS: + return "A DS record holds information about DNSSEC (DNS Security " + "Extensions) for your domain, helping with verification and " + "integrity."; + case DNSRecordType.TLS: + return "A TLS record is used for specifying details about how to " + "establish secure connections (like TLS certificates) for your" + " domain."; + case DNSRecordType.SRV: + return "An SRV record specifies the location of servers for specific" + " services, such as SIP, XMPP, or Minecraft servers."; + case DNSRecordType.TXT: + return "A TXT record allows you to add arbitrary text to your domain's" + " DNS record, often used for verification (e.g., SPF, DKIM)."; + case DNSRecordType.IMPORT: + return "An IMPORT record is used to bring in DNS records from an" + " external source into your domain's configuration."; + case DNSRecordType.SSH: + return "An SSH record provides information related to SSH public keys" + " for securely connecting to your domain's services."; + } + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart new file mode 100644 index 000000000..ad59c4fc1 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart @@ -0,0 +1,234 @@ +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../route_generator.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/assets.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/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/stack_dialog.dart'; +import 'add_dns_step_2.dart'; + +class AddDnsStep1 extends StatefulWidget { + const AddDnsStep1({super.key}); + + @override + State createState() => _AddDnsStep1State(); +} + +class _AddDnsStep1State extends State { + DNSRecordType? _recordType; + + bool _nextLock = false; + void _next() { + if (_nextLock) return; + _nextLock = true; + try { + if (mounted) { + Navigator.of(context).push( + RouteGenerator.getRoute( + builder: (context) { + return Util.isDesktop + ? DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep2( + recordType: _recordType!, + ), + ), + ], + ), + ) + : StackDialogBase( + child: AddDnsStep2(recordType: _recordType!), + ); + }, + ), + ); + } + } finally { + _nextLock = false; + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!Util.isDesktop) + Text( + "Add DNS record", + style: STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Text( + "Choose a record type", + style: Util.isDesktop + ? STextStyles.w500_12(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ) + : STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + SizedBox( + height: Util.isDesktop ? 12 : 8, + ), + DropdownButtonHideUnderline( + child: DropdownButton2( + hint: Text( + "Choose a record type", + style: STextStyles.fieldLabel(context), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + ), + isExpanded: true, + value: _recordType, + onChanged: (value) { + if (value is DNSRecordType && _recordType != value) { + setState(() { + _recordType = value; + }); + } + }, + iconStyleData: IconStyleData( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), + ), + items: [ + ...DNSRecordType.values.map( + (e) => DropdownMenuItem( + value: e, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + e.name, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ), + ), + ], + ), + ), + if (_recordType != null) + SizedBox( + height: Util.isDesktop ? 10 : 6, + ), + if (_recordType != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: Text( + _recordType!.info, + style: Util.isDesktop + ? STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_8(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + enabled: _recordType != null, + onPressed: _next, + buttonHeight: ButtonHeight.l, + ), + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 32 : 16, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart new file mode 100644 index 000000000..13b127bfc --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; + +import '../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import 'name_form_interface.dart'; +import 'sub_widgets/a_form.dart'; +import 'sub_widgets/cname_form.dart'; +import 'sub_widgets/ds_form.dart'; +import 'sub_widgets/import_form.dart'; +import 'sub_widgets/ns_form.dart'; +import 'sub_widgets/srv_form.dart'; +import 'sub_widgets/ssh_form.dart'; +import 'sub_widgets/tls_form.dart'; +import 'sub_widgets/txt_form.dart'; + +class AddDnsStep2 extends StatefulWidget { + const AddDnsStep2({super.key, required this.recordType}); + final DNSRecordType recordType; + + @override + State createState() => _AddDnsStep2State(); +} + +class _AddDnsStep2State extends State { + final GlobalKey _formStateKey = GlobalKey(); + + bool _nextLock = false; + void _nextPressed() { + if (_nextLock) return; + _nextLock = true; + try { + final record = _formStateKey.currentState!.buildRecord(); + Navigator.of(context, rootNavigator: true).pop( + record, + ); + } catch (e, s) { + Logging.instance.e( + runtimeType, + error: e, + stackTrace: s, + ); + + final String err; + switch (e.runtimeType) { + case const (ArgumentError): + err = e.toString().replaceFirst( + "Invalid Arguments(s): ", + "", + ); + + case const (Exception): + err = e.toString().replaceFirst( + "Exception: ", + "", + ); + + default: + err = e.toString(); + } + + showDialog( + context: context, + useRootNavigator: true, + builder: (context) { + return StackOkDialog( + desktopPopRootNavigator: true, // mobile as well due to sub nav flow + title: "Error", + maxWidth: 500, + message: err, + ); + }, + ); + } finally { + _nextLock = false; + } + } + + NameFormStatefulWidget? _form; + NameFormStatefulWidget get form => _form ??= _buildForm(); + + NameFormStatefulWidget _buildForm() { + switch (widget.recordType) { + case DNSRecordType.A: + return AForm(key: _formStateKey); + case DNSRecordType.CNAME: + return CNAMEForm(key: _formStateKey); + case DNSRecordType.NS: + return NSForm(key: _formStateKey); + case DNSRecordType.DS: + return DSForm(key: _formStateKey); + case DNSRecordType.TLS: + return TLSForm(key: _formStateKey); + case DNSRecordType.SRV: + return SRVForm(key: _formStateKey); + case DNSRecordType.TXT: + return TXTForm(key: _formStateKey); + case DNSRecordType.IMPORT: + return IMPORTForm(key: _formStateKey); + case DNSRecordType.SSH: + return SSHForm(key: _formStateKey); + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!Util.isDesktop) + Text( + "Add DNS record", + style: STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + form, + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Next", + onPressed: _nextPressed, + buttonHeight: ButtonHeight.l, + ), + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 32 : 16, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart new file mode 100644 index 000000000..6fff33894 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +import '../../../models/namecoin_dns/dns_record.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; + +abstract class NameFormStatefulWidget extends StatefulWidget { + const NameFormStatefulWidget({super.key}); +} + +abstract class NameFormState + extends State { + DNSRecord buildRecord(); +} + +class DNSFieldText extends StatelessWidget { + const DNSFieldText(this.text, {super.key}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + text, + style: Util.isDesktop + ? STextStyles.w500_12(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ) + : STextStyles.w500_14(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ); + } +} + +class DNSFormField extends StatelessWidget { + const DNSFormField({super.key, required this.controller, this.keyboardType}); + + final TextEditingController controller; + final TextInputType? keyboardType; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + controller: controller, + textAlignVertical: TextAlignVertical.center, + keyboardType: keyboardType, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + fillColor: Colors.transparent, + // hintText: "e.g. ns1.stackwallet.com.", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart new file mode 100644 index 000000000..fe120f17d --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart @@ -0,0 +1,216 @@ +import 'dart:io'; + +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../../models/namecoin_dns/dns_a_record_address_type.dart'; +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../themes/stack_colors.dart'; +import '../../../../utilities/assets.dart'; +import '../../../../utilities/constants.dart'; +import '../../../../utilities/text_styles.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class AForm extends NameFormStatefulWidget { + const AForm({super.key}); + + @override + NameFormState createState() => _AFormState(); +} + +class _AFormState extends NameFormState { + final _addressDataController = TextEditingController(); + final _addressDataFieldFocus = FocusNode(); + + DNSAddressType? _addressType; + + @override + DNSRecord buildRecord() { + final parts = _addressDataController.text.split(",").map((e) => e.trim()); + + final List addresses = []; + + for (final part in parts) { + switch (_addressType!) { + case DNSAddressType.IPv4: + final address = + InternetAddress(part.trim(), type: InternetAddressType.IPv4); + addresses.add(address.address); + break; + + case DNSAddressType.IPv6: + final address = InternetAddress(part, type: InternetAddressType.IPv6); + addresses.add(address.address); + break; + + case DNSAddressType.Tor: + final regex = RegExp(r'^[a-z2-7]{56}\.onion$'); + if (regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid tor address: $part"); + } + + case DNSAddressType.Freenet: + // TODO: verify + final regex = RegExp(r'(CHK|SSK|USK)@[a-zA-Z0-9~-]{43,}/?'); + final kskRegex = RegExp(r'KSK@[\w\-.~]+'); + if (regex.hasMatch(part) || kskRegex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid freenet address: $part"); + } + + case DNSAddressType.I2P: + // TODO: verify + final b32Regex = RegExp(r'^[a-z2-7]{52}\.b32\.i2p$'); + final b64Regex = RegExp(r'^[A-Za-z0-9+/=]{516,}$'); + if (b32Regex.hasMatch(part) || b64Regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid i2p address: $part"); + } + + case DNSAddressType.ZeroNet: + // TODO: verify + final regex = RegExp(r'^[13][a-km-zA-HJ-NP-Z1-9]{32,33}$'); + if (regex.hasMatch(part)) { + addresses.add(part); + } else { + throw Exception("Invalid zeronet address: $part"); + } + } + } + + final Map map; + + if (_addressType == DNSAddressType.Tor) { + map = { + "map": { + "_tor": { + "txt": addresses, + }, + }, + }; + } else { + map = { + _addressType!.key: addresses, + }; + } + + return DNSRecord( + type: DNSRecordType.A, + data: map, + ); + } + + @override + void dispose() { + _addressDataController.dispose(); + _addressDataFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DNSFieldText( + "Address type", + ), + DropdownButtonHideUnderline( + child: DropdownButton2( + hint: Text( + "Choose address type", + style: STextStyles.fieldLabel(context), + ), + dropdownStyleData: DropdownStyleData( + offset: const Offset(0, -10), + elevation: 0, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + menuItemStyleData: const MenuItemStyleData( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + ), + isExpanded: true, + value: _addressType, + onChanged: (value) { + if (value is DNSAddressType && _addressType != value) { + setState(() { + _addressType = value; + }); + } + }, + iconStyleData: IconStyleData( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), + ), + items: [ + ...DNSAddressType.values.map( + (e) => DropdownMenuItem( + value: e, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Text( + e.name, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + focusNode: _addressDataFieldFocus, + controller: _addressDataController, + textAlignVertical: TextAlignVertical.center, + maxLines: 3, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.all(16), + fillColor: Colors.transparent, + hintText: "e.g. 255.255.255.255, " + "76f4a520a262c269dcba66bc1f560452e30a44e14ce6b37ce20b8.onion", + hintStyle: STextStyles.fieldLabel(context), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart new file mode 100644 index 000000000..0c83a6b56 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class CNAMEForm extends NameFormStatefulWidget { + const CNAMEForm({super.key}); + + @override + NameFormState createState() => _CNAMEFormState(); +} + +class _CNAMEFormState extends NameFormState { + final _aliasController = TextEditingController(); + + @override + DNSRecord buildRecord() { + final address = _aliasController.text.trim(); + + return DNSRecord( + type: DNSRecordType.CNAME, + data: {"alias": address}, + ); + } + + @override + void dispose() { + _aliasController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Alias of", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _aliasController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart new file mode 100644 index 000000000..dbab0d28f --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class DSForm extends NameFormStatefulWidget { + const DSForm({super.key}); + + @override + NameFormState createState() => _DSFormState(); +} + +class _DSFormState extends NameFormState { + final _keytagController = TextEditingController(); + final _algoController = TextEditingController(); + final _typeController = TextEditingController(); + final _hashController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.DS, + data: { + "ds": [ + [ + int.parse(_keytagController.text.trim()), + int.parse(_algoController.text.trim()), + int.parse(_typeController.text.trim()), + _hashController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _keytagController.dispose(); + _algoController.dispose(); + _typeController.dispose(); + _hashController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Keytag", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _keytagController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Algorithm", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _algoController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Hash type", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _typeController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Hash (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _hashController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart new file mode 100644 index 000000000..02c95a3ee --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class IMPORTForm extends NameFormStatefulWidget { + const IMPORTForm({super.key}); + + @override + NameFormState createState() => _IMPORTFormState(); +} + +class _IMPORTFormState extends NameFormState { + final _nameController = TextEditingController(); + final _subdomainController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.IMPORT, + data: { + "import": [ + [ + _nameController.text.trim(), + if (_subdomainController.text.trim().isNotEmpty) + _subdomainController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _nameController.dispose(); + _subdomainController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Namecoin name", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _nameController, + ), + const DNSFieldText( + "Subdomain (optional)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _subdomainController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart new file mode 100644 index 000000000..604a5736c --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class NSForm extends NameFormStatefulWidget { + const NSForm({super.key}); + + @override + NameFormState createState() => _NSFormState(); +} + +class _NSFormState extends NameFormState { + final _serverController = TextEditingController(); + + @override + DNSRecord buildRecord() { + final address = _serverController.text.trim(); + + return DNSRecord( + type: DNSRecordType.NS, + data: { + "ns": [address], + }, + ); + } + + @override + void dispose() { + _serverController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Nameserver", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _serverController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart new file mode 100644 index 000000000..dff066abf --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class SRVForm extends NameFormStatefulWidget { + const SRVForm({super.key}); + + @override + NameFormState createState() => _SRVFormState(); +} + +class _SRVFormState extends NameFormState { + final _priorityController = TextEditingController(); + final _weightController = TextEditingController(); + final _portController = TextEditingController(); + final _hostController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.SRV, + data: { + "srv": [ + [ + int.parse(_priorityController.text.trim()), + int.parse(_weightController.text.trim()), + int.parse(_portController.text.trim()), + _hostController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _priorityController.dispose(); + _weightController.dispose(); + _portController.dispose(); + _hostController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Priority", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _priorityController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Weight", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _weightController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Port", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _portController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Host", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _hostController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart new file mode 100644 index 000000000..9be1cbb8b --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class SSHForm extends NameFormStatefulWidget { + const SSHForm({super.key}); + + @override + NameFormState createState() => _SSHFormState(); +} + +class _SSHFormState extends NameFormState { + final _algoController = TextEditingController(); + final _fingerprintTypeController = TextEditingController(); + final _fingerprintController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.SSH, + data: { + "sshfp": [ + [ + int.parse(_algoController.text.trim()), + int.parse(_fingerprintTypeController.text.trim()), + _fingerprintController.text.trim(), + ], + ], + }, + ); + } + + @override + void dispose() { + _algoController.dispose(); + _fingerprintTypeController.dispose(); + _fingerprintController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Algorithm", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _algoController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Fingerprint type", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _fingerprintTypeController, + keyboardType: TextInputType.number, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + const DNSFieldText( + "Fingerprint (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _fingerprintController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart new file mode 100644 index 000000000..bf2b1a33e --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class TLSForm extends NameFormStatefulWidget { + const TLSForm({super.key}); + + @override + NameFormState createState() => _TLSFormState(); +} + +class _TLSFormState extends NameFormState { + final _pubkeyController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.TLS, + data: { + "map": { + "*": { + "tls": [ + [ + 2, + 1, + 0, + _pubkeyController.text.trim(), + ], + ], + }, + }, + }, + ); + } + + @override + void dispose() { + _pubkeyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "DANE-TA public key (base64)", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _pubkeyController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart new file mode 100644 index 000000000..64f2111b7 --- /dev/null +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import '../../../../models/namecoin_dns/dns_record.dart'; +import '../../../../models/namecoin_dns/dns_record_type.dart'; +import '../../../../utilities/util.dart'; +import '../name_form_interface.dart'; + +class TXTForm extends NameFormStatefulWidget { + const TXTForm({super.key}); + + @override + NameFormState createState() => _TXTFormState(); +} + +class _TXTFormState extends NameFormState { + final _valueController = TextEditingController(); + + @override + DNSRecord buildRecord() { + return DNSRecord( + type: DNSRecordType.TXT, + data: { + "txt": [_valueController.text.trim()], + }, + ); + } + + @override + void dispose() { + _valueController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const DNSFieldText( + "Value", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), + DNSFormField( + controller: _valueController, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart new file mode 100644 index 000000000..b9a30a24f --- /dev/null +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -0,0 +1,458 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../providers/providers.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../models/namecoin_dns/dns_record.dart'; +import '../../route_generator.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/text_styles.dart'; +import '../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/custom_buttons/blue_text_button.dart'; +import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/rounded_white_container.dart'; +import 'add_dns_record/add_dns_step_1.dart'; +import 'confirm_name_transaction_view.dart'; + +class BuyDomainView extends ConsumerStatefulWidget { + const BuyDomainView({ + super.key, + required this.walletId, + required this.domainName, + }); + + final String walletId; + final String domainName; + + static const routeName = "/buyDomainView"; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + bool _settingsHidden = true; + final List _dnsRecords = []; + + String _getFormattedDNSRecords() { + if (_dnsRecords.isEmpty) return ""; + + return DNSRecord.merge(_dnsRecords); + } + + bool _preRegLock = false; + Future _preRegister() async { + if (_preRegLock) return; + _preRegLock = true; + try { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + final myAddress = await wallet.getCurrentReceivingAddress(); + if (myAddress == null) { + throw Exception("No receiving address found"); + } + + final value = _getFormattedDNSRecords(); + + Logging.instance.f(value); + + // get address private key for deterministic salt + final pk = await wallet.getPrivateKey(myAddress); + + String formattedName = widget.domainName; + if (!formattedName.startsWith("d/")) { + formattedName = "d/$formattedName"; + } + if (formattedName.endsWith(".bit")) { + formattedName.split(".bit").first; + } + + final data = scriptNameNew(formattedName, pk.data); + + TxData txData = TxData( + opNameState: NameOpState( + name: formattedName, + saltHex: data.$2, + commitment: data.$3, + value: value, + nameScriptHex: data.$1, + type: OpName.nameNew, + outputPosition: -1, //currently unknown, updated later + ), + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: myAddress.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + ); + + txData = await wallet.prepareNameSend(txData: txData); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: MediaQuery.of(context).size.height - 64, + maxWidth: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: wallet.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, wallet.walletId), + ); + } + } + } catch (e, s) { + Logging.instance.e("_preRegister failed", error: e, stackTrace: s); + } finally { + _preRegLock = false; + } + } + + bool _addLock = false; + Future _addRecord() async { + if (_addLock) return; + _addLock = true; + try { + final value = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return Navigator( + onGenerateRoute: (settings) { + return RouteGenerator.getRoute( + builder: (context) { + return Util.isDesktop + ? DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, rootNavigator: true) + .pop(); + }, + ), + ], + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: AddDnsStep1(), + ), + ], + ), + ) + : const StackDialogBase( + child: AddDnsStep1(), + ); + }, + ); + }, + ); + }, + ); + + if (mounted && value != null) { + setState(() { + _dnsRecords.add(value); + }); + } + } catch (e, s) { + Logging.instance.e("Add DNS record failed", error: e, stackTrace: s); + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Add DNS record failed", + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _addLock = false; + } + } + + @override + Widget build(BuildContext context) { + final coin = ref.watch(pWalletCoin(widget.walletId)); + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + "Buy domain", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, + children: [ + if (!Util.isDesktop) + Text( + "Buy domain", + style: Util.isDesktop + ? STextStyles.desktopH3(context) + : STextStyles.pageTitleH2(context), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Name registration will take approximately 2 to 4 hours.", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + ], + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Domain name", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + Text( + "${widget.domainName.substring(2)}.bit", + style: Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ) + : STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .infoItemLabel, + ), + ), + Text( + ref.watch(pAmountFormatter(coin)).format( + Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: coin.fractionDigits, + ), + ), + style: Util.isDesktop + ? STextStyles.w500_14(context) + : STextStyles.w500_12(context), + ), + ], + ), + ), + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + CustomTextButton( + text: _settingsHidden ? "More settings" : "Hide settings", + onTap: () { + setState(() { + _settingsHidden = !_settingsHidden; + }); + }, + ), + if (!_settingsHidden) + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + if (!_settingsHidden) + if (_dnsRecords.isEmpty) + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Add DNS records to your domain name", + style: STextStyles.w500_12(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + if (!_settingsHidden) + if (_dnsRecords.isNotEmpty) + ListView( + shrinkWrap: true, + children: [ + ..._dnsRecords.map( + (e) => DNSRecordCard( + key: Key(e.toString()), + record: e, + onRemoveTapped: () => setState(() { + _dnsRecords.remove(e); + }), + ), + ), + ], + ), + if (!_settingsHidden) + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + if (!_settingsHidden) + SecondaryButton( + label: _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _addRecord, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + if (!Util.isDesktop) const Spacer(), + PrimaryButton( + label: "Buy", + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _preRegister, + ), + SizedBox( + height: Util.isDesktop ? 32 : 16, + ), + ], + ), + ); + } +} + +class DNSRecordCard extends StatelessWidget { + const DNSRecordCard({ + super.key, + required this.record, + required this.onRemoveTapped, + this.extraInfo, + }); + + final DNSRecord record; + final VoidCallback onRemoveTapped; + final String? extraInfo; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${record.type.name}${extraInfo != null ? " - ${extraInfo!}" : ""}", + ), + CustomTextButton( + text: "Remove", + onTap: onRemoveTapped, + ), + ], + ), + Text(record.jsonDataString()), + ], + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart index feb9b93ce..cca3d4e79 100644 --- a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -3,26 +3,23 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:namecoin/namecoin.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; -import '../../../utilities/amount/amount.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; -import '../../../wallets/models/name_op_state.dart'; -import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.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_white_container.dart'; import '../../../widgets/stack_dialog.dart'; -import '../confirm_name_transaction_view.dart'; +import '../buy_domain_view.dart'; class BuyDomainOptionWidget extends ConsumerStatefulWidget { const BuyDomainOptionWidget({super.key, required this.walletId}); @@ -248,68 +245,6 @@ class _NameCard extends ConsumerWidget { final bool isAvailable; final String formattedName; - Future _preRegister( - BuildContext context, - NamecoinWallet wallet, - String value, - ) async { - final myAddress = await wallet.getCurrentReceivingAddress(); - if (myAddress == null) { - throw Exception("No receiving address found"); - } - - // get address private key for deterministic salt - final pk = await wallet.getPrivateKey(myAddress); - - final data = scriptNameNew(formattedName, pk.data); - - TxData txData = TxData( - opNameState: NameOpState( - name: formattedName, - saltHex: data.$2, - commitment: data.$3, - value: value, - nameScriptHex: data.$1, - type: OpName.nameNew, - outputPosition: -1, //currently unknown, updated later - ), - feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? - recipients: [ - ( - address: myAddress.value, - isChange: false, - amount: Amount( - rawValue: BigInt.from(kNameNewAmountSats), - fractionDigits: wallet.cryptoCurrency.fractionDigits, - ), - ), - ], - ); - - txData = await wallet.prepareNameSend(txData: txData); - - if (context.mounted) { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: wallet.walletId, - ), - ), - ); - } else { - await Navigator.of(context).pushNamed( - ConfirmNameTransactionView.routeName, - arguments: (txData, wallet.walletId), - ); - } - } - } - @override Widget build(BuildContext context, WidgetRef ref) { final availability = isAvailable ? "Available" : "Unavailable"; @@ -349,10 +284,55 @@ class _NameCard extends ConsumerWidget { enabled: isAvailable, buttonHeight: ButtonHeight.m, width: 140, - onPressed: () => _preRegister( - context, - ref.read(pWallets).getWallet(walletId) as NamecoinWallet, - ), + onPressed: () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 580, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy domain", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuyDomainView( + walletId: walletId, + domainName: formattedName, + ), + ), + ], + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + BuyDomainView.routeName, + arguments: ( + walletId: walletId, + domainName: formattedName + ), + ); + } + } + }, ), ], ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 1a3581db1..3e51359ae 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -72,6 +72,7 @@ import 'pages/home_view/home_view.dart'; import 'pages/intro_view.dart'; import 'pages/manage_favorites_view/manage_favorites_view.dart'; import 'pages/monkey/monkey_view.dart'; +import 'pages/namecoin_names/buy_domain_view.dart'; import 'pages/namecoin_names/confirm_name_transaction_view.dart'; import 'pages/namecoin_names/namecoin_names_home_view.dart'; import 'pages/notification_views/notifications_view.dart'; @@ -2186,6 +2187,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case BuyDomainView.routeName: + if (args is ({String walletId, String domainName})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyDomainView( + walletId: args.walletId, + domainName: args.domainName, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: if (args is bool) { From 54e9c97265745f773fc07aedb50d514905e9910e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Feb 2025 16:18:51 -0600 Subject: [PATCH 14/29] add method to check for and ignore name outputs in balance --- lib/wallets/wallet/impl/namecoin_wallet.dart | 9 +++++++++ lib/wallets/wallet/intermediate/bip39_hd_wallet.dart | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index ff7bce0cf..bb28ed52e 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -1368,6 +1368,15 @@ class NamecoinWallet return txData; } + @override + bool ignoreUtxoInBalance(UTXO utxo) { + if (getOpNameDataFrom(utxo) != null) { + // ignore name outputs in balance calculation + return true; + } + return false; + } + /// return null if utxo does not contain name op OpNameData? getOpNameDataFrom(UTXO utxo) { if (utxo.otherData == null) { diff --git a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart index 90cbacea4..777dda064 100644 --- a/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart +++ b/lib/wallets/wallet/intermediate/bip39_hd_wallet.dart @@ -6,6 +6,7 @@ import 'package:isar/isar.dart'; import '../../../models/balance.dart'; import '../../../models/isar/models/blockchain_data/address.dart'; +import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../models/keys/view_only_wallet_data.dart'; import '../../../utilities/amount/amount.dart'; import '../../../utilities/enums/derive_path_type_enum.dart'; @@ -194,6 +195,9 @@ abstract class Bip39HDWallet extends Bip39Wallet return address; } + /// If this function returns true, the UTXO will be ignored in displayed balance + bool ignoreUtxoInBalance(UTXO utxo) => false; + // ========== Private ======================================================== Future _viewOnlyPathHelper() async { @@ -329,6 +333,8 @@ abstract class Bip39HDWallet extends Bip39Wallet ); for (final utxo in utxos) { + if (ignoreUtxoInBalance(utxo)) continue; + final utxoAmount = Amount( rawValue: BigInt.from(utxo.value), fractionDigits: cryptoCurrency.fractionDigits, From 3314278cb6e99248bb70ddad66eac1cc8e207bdc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Feb 2025 16:56:04 -0600 Subject: [PATCH 15/29] clean up record info display --- lib/models/namecoin_dns/dns_record.dart | 10 +++++++++- lib/pages/namecoin_names/buy_domain_view.dart | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/models/namecoin_dns/dns_record.dart b/lib/models/namecoin_dns/dns_record.dart index ee0842b34..63958d748 100644 --- a/lib/models/namecoin_dns/dns_record.dart +++ b/lib/models/namecoin_dns/dns_record.dart @@ -16,7 +16,15 @@ final class DNSRecord { required this.data, }); - String jsonDataString() => jsonEncode(data); + String getValueString() { + // TODO error handling + dynamic value = data; + while (value is Map) { + value = value[value.keys.first]; + } + + return value.toString(); + } DNSRecord copyWith({ DNSRecordType? type, diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index b9a30a24f..977656cc5 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -15,7 +15,9 @@ import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/stack_dialog.dart'; +import '../../models/namecoin_dns/dns_a_record_address_type.dart'; import '../../models/namecoin_dns/dns_record.dart'; +import '../../models/namecoin_dns/dns_record_type.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount_formatter.dart'; @@ -425,12 +427,19 @@ class DNSRecordCard extends StatelessWidget { super.key, required this.record, required this.onRemoveTapped, - this.extraInfo, }); final DNSRecord record; final VoidCallback onRemoveTapped; - final String? extraInfo; + + String get _extraInfo { + if (record.type == DNSRecordType.A) { + // TODO error handling + return " - ${DNSAddressType.values.firstWhere((e) => e.key == record.data.keys.first).name}"; + } + + return ""; + } @override Widget build(BuildContext context) { @@ -442,7 +451,7 @@ class DNSRecordCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${record.type.name}${extraInfo != null ? " - ${extraInfo!}" : ""}", + "${record.type.name}$_extraInfo", ), CustomTextButton( text: "Remove", @@ -450,7 +459,7 @@ class DNSRecordCard extends StatelessWidget { ), ], ), - Text(record.jsonDataString()), + Text(record.getValueString()), ], ), ); From 882571029202a63ff5d61550332f65fdc1908116 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Feb 2025 17:36:17 -0600 Subject: [PATCH 16/29] better base dialog widget --- lib/pages/namecoin_names/buy_domain_view.dart | 75 ++++++++++--------- .../sub_widgets/buy_domain_option_widget.dart | 58 +++++++------- lib/widgets/dialogs/s_dialog.dart | 64 ++++++++++++++++ 3 files changed, 133 insertions(+), 64 deletions(-) create mode 100644 lib/widgets/dialogs/s_dialog.dart diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index 977656cc5..ea7c032b3 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -11,7 +11,6 @@ import '../../../utilities/util.dart'; import '../../../wallets/models/name_op_state.dart'; import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.dart'; -import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/stack_dialog.dart'; @@ -28,6 +27,7 @@ import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/custom_buttons/blue_text_button.dart'; import '../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../widgets/dialogs/s_dialog.dart'; import '../../widgets/rounded_white_container.dart'; import 'add_dns_record/add_dns_step_1.dart'; import 'confirm_name_transaction_view.dart'; @@ -116,12 +116,13 @@ class _BuyDomainWidgetState extends ConsumerState { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxHeight: MediaQuery.of(context).size.height - 64, - maxWidth: 580, - child: ConfirmNameTransactionView( - txData: txData, - walletId: wallet.walletId, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: wallet.walletId, + ), ), ), ); @@ -153,37 +154,39 @@ class _BuyDomainWidgetState extends ConsumerState { return RouteGenerator.getRoute( builder: (context) { return Util.isDesktop - ? DesktopDialog( - maxHeight: double.infinity, - maxWidth: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + ? SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add DNS record", + style: STextStyles.desktopH3(context), + ), ), - child: Text( - "Add DNS record", - style: STextStyles.desktopH3(context), + DesktopDialogCloseButton( + onPressedOverride: () { + Navigator.of(context, + rootNavigator: true) + .pop(); + }, ), - ), - DesktopDialogCloseButton( - onPressedOverride: () { - Navigator.of(context, rootNavigator: true) - .pop(); - }, - ), - ], - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 32), - child: AddDnsStep1(), - ), - ], + ], + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 32), + child: AddDnsStep1(), + ), + ], + ), ), ) : const StackDialogBase( diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart index cca3d4e79..14d902e88 100644 --- a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -13,10 +13,10 @@ import '../../../utilities/show_loading.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.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/dialogs/s_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; import '../../../widgets/stack_dialog.dart'; import '../buy_domain_view.dart'; @@ -289,36 +289,38 @@ class _NameCard extends ConsumerWidget { if (Util.isDesktop) { await showDialog( context: context, - builder: (context) => DesktopDialog( - maxWidth: 580, - maxHeight: double.infinity, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy domain", + style: STextStyles.desktopH3(context), + ), ), - child: Text( - "Buy domain", - style: STextStyles.desktopH3(context), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, + const DesktopDialogCloseButton(), + ], ), - child: BuyDomainView( - walletId: walletId, - domainName: formattedName, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: BuyDomainView( + walletId: walletId, + domainName: formattedName, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/widgets/dialogs/s_dialog.dart b/lib/widgets/dialogs/s_dialog.dart new file mode 100644 index 000000000..a6b32148c --- /dev/null +++ b/lib/widgets/dialogs/s_dialog.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../../themes/stack_colors.dart'; +import '../../utilities/util.dart'; +import '../conditional_parent.dart'; + +class SDialog extends StatelessWidget { + const SDialog({ + super.key, + required this.child, + this.padding = EdgeInsets.zero, + this.contentCanScroll = true, + this.margin, + this.background, + this.mainAxisAlignment, + this.crossAxisAlignment, + }); + + final Widget child; + final bool contentCanScroll; + final Color? background; + final EdgeInsets? margin; + final EdgeInsets padding; + final MainAxisAlignment? mainAxisAlignment; + final CrossAxisAlignment? crossAxisAlignment; + + @override + Widget build(BuildContext context) { + return Padding( + padding: margin ?? EdgeInsets.all(Util.isDesktop ? 32 : 16), + child: Column( + mainAxisAlignment: mainAxisAlignment ?? + (Util.isDesktop ? MainAxisAlignment.center : MainAxisAlignment.end), + crossAxisAlignment: crossAxisAlignment ?? CrossAxisAlignment.center, + children: [ + Flexible( + child: Material( + borderRadius: BorderRadius.circular(20), + child: Container( + decoration: BoxDecoration( + color: background ?? + Theme.of(context).extension()!.popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: ConditionalParent( + condition: contentCanScroll, + builder: (child) => SingleChildScrollView( + child: child, + ), + child: Padding( + padding: padding, + child: child, + ), + ), + ), + ), + ), + ], + ), + ); + } +} From 4b86a592767300802246b5ab9c79086eb920510b Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Feb 2025 10:59:38 -0600 Subject: [PATCH 17/29] validate name value length --- lib/models/namecoin_dns/dns_record.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/models/namecoin_dns/dns_record.dart b/lib/models/namecoin_dns/dns_record.dart index 63958d748..0c4bef782 100644 --- a/lib/models/namecoin_dns/dns_record.dart +++ b/lib/models/namecoin_dns/dns_record.dart @@ -1,8 +1,9 @@ import 'dart:convert'; import 'package:meta/meta.dart'; +import 'package:namecoin/namecoin.dart'; -import '../../utilities/logger.dart'; +import '../../utilities/extensions/extensions.dart'; import 'dns_a_record_address_type.dart'; import 'dns_record_type.dart'; @@ -43,8 +44,6 @@ final class DNSRecord { } static String merge(List records) { - final start = DateTime.now(); - final Map result = {}; for (final record in records) { @@ -69,8 +68,15 @@ final class DNSRecord { } } - Logging.instance.w(DateTime.now().difference(start)); - return jsonEncode(result); + final string = jsonEncode(result); + if (string.toUint8ListFromUtf8.length > valueMaxLength) { + throw Exception( + "Value length (${string.toUint8ListFromUtf8.length}) exceeds maximum" + " allowed ($valueMaxLength)", + ); + } + + return string; } } From 7ee9db96a124993a59435d31e4ec8190c0be9aea Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Feb 2025 11:00:03 -0600 Subject: [PATCH 18/29] show error dialog --- lib/pages/namecoin_names/buy_domain_view.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index ea7c032b3..22f16d738 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -135,6 +135,23 @@ class _BuyDomainWidgetState extends ConsumerState { } } catch (e, s) { Logging.instance.e("_preRegister failed", error: e, stackTrace: s); + + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Add DNS record failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } } finally { _preRegLock = false; } From 7ce16e0366e4e6d9b165f5c48d9c4245a1d0b5a7 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Feb 2025 11:00:33 -0600 Subject: [PATCH 19/29] better encoding --- lib/wallets/wallet/impl/namecoin_wallet.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index bb28ed52e..cb3901e29 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -32,8 +32,6 @@ const kNameTxDefaultFeeRate = FeeRateType.slow; const kNameNewAmountSats = 150_0000; const kNameAmountSats = 100_0000; -const _kNameSaltSplitter = r"$$$$"; - String nameSaltKeyBuilder(String txid, String walletId, int txPos) { if (txPos.isNegative) { throw Exception("Invalid vout index"); @@ -43,11 +41,16 @@ String nameSaltKeyBuilder(String txid, String walletId, int txPos) { } String encodeNameSaltData(String name, String salt, String value) => - "$name$_kNameSaltSplitter$salt$_kNameSaltSplitter$value"; + jsonEncode({ + "name": name, + "salt": salt, + "value": value, + }); + ({String salt, String name, String value}) decodeNameSaltData(String value) { try { - final split = value.split(_kNameSaltSplitter); - return (salt: split[1], name: split[0], value: split[2]); + final map = (jsonDecode(value) as Map).cast(); + return (salt: map["salt"]!, name: map["name"]!, value: map["value"]!); } catch (_) { throw Exception("Bad name salt data"); } From f5d2382a44849d9a2cdd1b2a53bb4911422c3cac Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Feb 2025 11:04:45 -0600 Subject: [PATCH 20/29] clean up logging --- lib/pages/namecoin_names/buy_domain_view.dart | 2 +- lib/wallets/wallet/impl/namecoin_wallet.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index 22f16d738..73a3f5772 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -72,7 +72,7 @@ class _BuyDomainWidgetState extends ConsumerState { final value = _getFormattedDNSRecords(); - Logging.instance.f(value); + Logging.instance.t("Formatted namecoin name value: $value"); // get address private key for deterministic salt final pk = await wallet.getPrivateKey(myAddress); diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index cb3901e29..87dd63acc 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -445,7 +445,6 @@ class NamecoinWallet for (final utxo in utxos) { final nameOp = getOpNameDataFrom(utxo); if (nameOp?.op == OpName.nameNew) { - Logging.instance.f(utxo); final sKey = nameSaltKeyBuilder(utxo.txid, walletId, utxo.vout); final encoded = await secureStorageInterface.read(key: sKey); From 64564c05a07188f0d2459150b85fb2963ec730f5 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Feb 2025 13:02:29 -0600 Subject: [PATCH 21/29] fix: pop buy name dialog on success --- lib/pages/namecoin_names/confirm_name_transaction_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index df1245da2..53e6017d2 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -160,6 +160,8 @@ class _ConfirmNameTransactionViewState Navigator.of(context, rootNavigator: Util.isDesktop).pop(); // pop confirm send view Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + // pop buy popup //TODO test on mobile + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); } } catch (e, s) { const niceError = "Broadcast name transaction failed"; From bafb558326694ac66bb850d60a71bdd80c978605 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Feb 2025 15:31:52 -0600 Subject: [PATCH 22/29] WIP: namecoin names desktop/mobile specific layout tweaks, and various clean up --- lib/models/namecoin_dns/dns_record.dart | 33 +- lib/models/namecoin_dns/dns_record_type.dart | 23 + .../add_dns_record/add_dns_step_1.dart | 47 +- .../add_dns_record/add_dns_step_2.dart | 37 +- .../add_dns_record/name_form_interface.dart | 6 +- .../add_dns_record/sub_widgets/a_form.dart | 41 +- .../sub_widgets/cname_form.dart | 3 +- .../add_dns_record/sub_widgets/ds_form.dart | 3 +- .../sub_widgets/import_form.dart | 3 +- .../add_dns_record/sub_widgets/ns_form.dart | 3 +- .../add_dns_record/sub_widgets/srv_form.dart | 3 +- .../add_dns_record/sub_widgets/ssh_form.dart | 3 +- .../add_dns_record/sub_widgets/tls_form.dart | 3 +- .../add_dns_record/sub_widgets/txt_form.dart | 3 +- lib/pages/namecoin_names/buy_domain_view.dart | 225 ++++--- .../confirm_name_transaction_view.dart | 6 +- .../namecoin_names_home_view.dart | 181 ++++-- .../sub_widgets/buy_domain_option_widget.dart | 6 +- .../manage_domains_option_widget.dart | 46 +- .../sub_widgets/name_details.dart | 595 ++++++++++++++++++ .../sub_widgets/owned_name_card.dart | 183 ++++++ lib/route_generator.dart | 16 + lib/wallets/wallet/impl/namecoin_wallet.dart | 6 + lib/widgets/desktop/desktop_scaffold.dart | 4 +- 24 files changed, 1277 insertions(+), 202 deletions(-) create mode 100644 lib/pages/namecoin_names/sub_widgets/name_details.dart create mode 100644 lib/pages/namecoin_names/sub_widgets/owned_name_card.dart diff --git a/lib/models/namecoin_dns/dns_record.dart b/lib/models/namecoin_dns/dns_record.dart index 0c4bef782..9c54bf98b 100644 --- a/lib/models/namecoin_dns/dns_record.dart +++ b/lib/models/namecoin_dns/dns_record.dart @@ -4,19 +4,44 @@ import 'package:meta/meta.dart'; import 'package:namecoin/namecoin.dart'; import '../../utilities/extensions/extensions.dart'; -import 'dns_a_record_address_type.dart'; import 'dns_record_type.dart'; @Immutable() -final class DNSRecord { +abstract class DNSRecordBase { + final String name; + + DNSRecordBase({required this.name}); + + String getValueString(); +} + +@Immutable() +final class RawDNSRecord extends DNSRecordBase { + final String value; + + RawDNSRecord({required super.name, required this.value}); + + @override + String getValueString() => value; + + @override + String toString() { + return "RawDNSRecord(name: $name, value: $value)"; + } +} + +@Immutable() +final class DNSRecord extends DNSRecordBase { final DNSRecordType type; final Map data; DNSRecord({ + required super.name, required this.type, required this.data, }); + @override String getValueString() { // TODO error handling dynamic value = data; @@ -29,18 +54,18 @@ final class DNSRecord { DNSRecord copyWith({ DNSRecordType? type, - DNSAddressType? addressType, Map? data, }) { return DNSRecord( type: type ?? this.type, data: data ?? this.data, + name: name, ); } @override String toString() { - return "DNSRecord(type: $type, data: $data)"; + return "DNSRecord(name: $name, type: $type, data: $data)"; } static String merge(List records) { diff --git a/lib/models/namecoin_dns/dns_record_type.dart b/lib/models/namecoin_dns/dns_record_type.dart index c8e215194..6e25cc038 100644 --- a/lib/models/namecoin_dns/dns_record_type.dart +++ b/lib/models/namecoin_dns/dns_record_type.dart @@ -42,4 +42,27 @@ enum DNSRecordType { " for securely connecting to your domain's services."; } } + + String? get key { + switch (this) { + case DNSRecordType.A: + return null; + case DNSRecordType.CNAME: + return "alias"; + case DNSRecordType.NS: + return "ns"; + case DNSRecordType.DS: + return "ds"; + case DNSRecordType.TLS: + return "tls"; + case DNSRecordType.SRV: + return "srv"; + case DNSRecordType.TXT: + return "txt"; + case DNSRecordType.IMPORT: + return "import"; + case DNSRecordType.SSH: + return "sshfp"; + } + } } diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart index ad59c4fc1..dd9119cac 100644 --- a/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_1.dart @@ -17,7 +17,9 @@ import '../../../widgets/stack_dialog.dart'; import 'add_dns_step_2.dart'; class AddDnsStep1 extends StatefulWidget { - const AddDnsStep1({super.key}); + const AddDnsStep1({super.key, required this.name}); + + final String name; @override State createState() => _AddDnsStep1State(); @@ -64,13 +66,19 @@ class _AddDnsStep1State extends State { ), child: AddDnsStep2( recordType: _recordType!, + name: widget.name, ), ), ], ), ) : StackDialogBase( - child: AddDnsStep2(recordType: _recordType!), + keyboardPaddingAmount: + MediaQuery.of(context).viewInsets.bottom, + child: AddDnsStep2( + recordType: _recordType!, + name: widget.name, + ), ); }, ), @@ -114,9 +122,20 @@ class _AddDnsStep1State extends State { "Choose a record type", style: STextStyles.fieldLabel(context), ), + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, + maxHeight: Util.isDesktop ? null : 200, decoration: BoxDecoration( color: Theme.of(context) .extension()! @@ -142,11 +161,14 @@ class _AddDnsStep1State extends State { } }, iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 10, - height: 5, - color: Theme.of(context).extension()!.textDark3, + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), ), ), items: [ @@ -206,7 +228,7 @@ class _AddDnsStep1State extends State { Expanded( child: SecondaryButton( label: "Cancel", - buttonHeight: ButtonHeight.l, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: () { Navigator.of(context, rootNavigator: true).pop(); }, @@ -220,14 +242,15 @@ class _AddDnsStep1State extends State { label: "Next", enabled: _recordType != null, onPressed: _next, - buttonHeight: ButtonHeight.l, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, ), ), ], ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + if (Util.isDesktop) + const SizedBox( + height: 32, + ), ], ); } diff --git a/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart index 13b127bfc..f6bb22f67 100644 --- a/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart +++ b/lib/pages/namecoin_names/add_dns_record/add_dns_step_2.dart @@ -19,7 +19,13 @@ import 'sub_widgets/tls_form.dart'; import 'sub_widgets/txt_form.dart'; class AddDnsStep2 extends StatefulWidget { - const AddDnsStep2({super.key, required this.recordType}); + const AddDnsStep2({ + super.key, + required this.recordType, + required this.name, + }); + + final String name; final DNSRecordType recordType; @override @@ -86,23 +92,23 @@ class _AddDnsStep2State extends State { NameFormStatefulWidget _buildForm() { switch (widget.recordType) { case DNSRecordType.A: - return AForm(key: _formStateKey); + return AForm(key: _formStateKey, name: widget.name); case DNSRecordType.CNAME: - return CNAMEForm(key: _formStateKey); + return CNAMEForm(key: _formStateKey, name: widget.name); case DNSRecordType.NS: - return NSForm(key: _formStateKey); + return NSForm(key: _formStateKey, name: widget.name); case DNSRecordType.DS: - return DSForm(key: _formStateKey); + return DSForm(key: _formStateKey, name: widget.name); case DNSRecordType.TLS: - return TLSForm(key: _formStateKey); + return TLSForm(key: _formStateKey, name: widget.name); case DNSRecordType.SRV: - return SRVForm(key: _formStateKey); + return SRVForm(key: _formStateKey, name: widget.name); case DNSRecordType.TXT: - return TXTForm(key: _formStateKey); + return TXTForm(key: _formStateKey, name: widget.name); case DNSRecordType.IMPORT: - return IMPORTForm(key: _formStateKey); + return IMPORTForm(key: _formStateKey, name: widget.name); case DNSRecordType.SSH: - return SSHForm(key: _formStateKey); + return SSHForm(key: _formStateKey, name: widget.name); } } @@ -129,7 +135,7 @@ class _AddDnsStep2State extends State { Expanded( child: SecondaryButton( label: "Cancel", - buttonHeight: ButtonHeight.l, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, onPressed: () { Navigator.of(context).pop(); }, @@ -142,14 +148,15 @@ class _AddDnsStep2State extends State { child: PrimaryButton( label: "Next", onPressed: _nextPressed, - buttonHeight: ButtonHeight.l, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, ), ), ], ), - SizedBox( - height: Util.isDesktop ? 32 : 16, - ), + if (Util.isDesktop) + const SizedBox( + height: 32, + ), ], ); } diff --git a/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart index 6fff33894..e5fc996bd 100644 --- a/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart +++ b/lib/pages/namecoin_names/add_dns_record/name_form_interface.dart @@ -7,7 +7,9 @@ import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; abstract class NameFormStatefulWidget extends StatefulWidget { - const NameFormStatefulWidget({super.key}); + const NameFormStatefulWidget({super.key, required this.name}); + + final String name; } abstract class NameFormState @@ -54,8 +56,6 @@ class DNSFormField extends StatelessWidget { decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.all(16), - fillColor: Colors.transparent, - // hintText: "e.g. ns1.stackwallet.com.", hintStyle: STextStyles.fieldLabel(context), border: InputBorder.none, enabledBorder: InputBorder.none, diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart index fe120f17d..ec6728f9a 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/a_form.dart @@ -15,7 +15,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class AForm extends NameFormStatefulWidget { - const AForm({super.key}); + const AForm({super.key, required super.name}); @override NameFormState createState() => _AFormState(); @@ -25,7 +25,7 @@ class _AFormState extends NameFormState { final _addressDataController = TextEditingController(); final _addressDataFieldFocus = FocusNode(); - DNSAddressType? _addressType; + DNSAddressType _addressType = DNSAddressType.IPv4; @override DNSRecord buildRecord() { @@ -34,7 +34,7 @@ class _AFormState extends NameFormState { final List addresses = []; for (final part in parts) { - switch (_addressType!) { + switch (_addressType) { case DNSAddressType.IPv4: final address = InternetAddress(part.trim(), type: InternetAddressType.IPv4); @@ -102,6 +102,7 @@ class _AFormState extends NameFormState { } return DNSRecord( + name: widget.name, type: DNSRecordType.A, data: map, ); @@ -123,6 +124,9 @@ class _AFormState extends NameFormState { const DNSFieldText( "Address type", ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), DropdownButtonHideUnderline( child: DropdownButton2( hint: Text( @@ -132,6 +136,7 @@ class _AFormState extends NameFormState { dropdownStyleData: DropdownStyleData( offset: const Offset(0, -10), elevation: 0, + maxHeight: Util.isDesktop ? null : 200, decoration: BoxDecoration( color: Theme.of(context) .extension()! @@ -156,12 +161,25 @@ class _AFormState extends NameFormState { }); } }, + buttonStyleData: ButtonStyleData( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), iconStyleData: IconStyleData( - icon: SvgPicture.asset( - Assets.svg.chevronDown, - width: 10, - height: 5, - color: Theme.of(context).extension()!.textDark3, + icon: Padding( + padding: const EdgeInsets.only(right: 10), + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 10, + height: 5, + color: Theme.of(context).extension()!.textDark3, + ), ), ), items: [ @@ -188,6 +206,12 @@ class _AFormState extends NameFormState { SizedBox( height: Util.isDesktop ? 24 : 16, ), + const DNSFieldText( + "Value", + ), + SizedBox( + height: Util.isDesktop ? 10 : 8, + ), ClipRRect( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, @@ -200,7 +224,6 @@ class _AFormState extends NameFormState { decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.all(16), - fillColor: Colors.transparent, hintText: "e.g. 255.255.255.255, " "76f4a520a262c269dcba66bc1f560452e30a44e14ce6b37ce20b8.onion", hintStyle: STextStyles.fieldLabel(context), diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart index 0c83a6b56..34cc7a600 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/cname_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class CNAMEForm extends NameFormStatefulWidget { - const CNAMEForm({super.key}); + const CNAMEForm({super.key, required super.name}); @override NameFormState createState() => _CNAMEFormState(); @@ -20,6 +20,7 @@ class _CNAMEFormState extends NameFormState { final address = _aliasController.text.trim(); return DNSRecord( + name: widget.name, type: DNSRecordType.CNAME, data: {"alias": address}, ); diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart index dbab0d28f..29cdbaf6f 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ds_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class DSForm extends NameFormStatefulWidget { - const DSForm({super.key}); + const DSForm({super.key, required super.name}); @override NameFormState createState() => _DSFormState(); @@ -21,6 +21,7 @@ class _DSFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.DS, data: { "ds": [ diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart index 02c95a3ee..cfcaa0d23 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/import_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class IMPORTForm extends NameFormStatefulWidget { - const IMPORTForm({super.key}); + const IMPORTForm({super.key, required super.name}); @override NameFormState createState() => _IMPORTFormState(); @@ -19,6 +19,7 @@ class _IMPORTFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.IMPORT, data: { "import": [ diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart index 604a5736c..6d6eb914c 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ns_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class NSForm extends NameFormStatefulWidget { - const NSForm({super.key}); + const NSForm({super.key, required super.name}); @override NameFormState createState() => _NSFormState(); @@ -20,6 +20,7 @@ class _NSFormState extends NameFormState { final address = _serverController.text.trim(); return DNSRecord( + name: widget.name, type: DNSRecordType.NS, data: { "ns": [address], diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart index dff066abf..db3c01a0c 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/srv_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class SRVForm extends NameFormStatefulWidget { - const SRVForm({super.key}); + const SRVForm({super.key, required super.name}); @override NameFormState createState() => _SRVFormState(); @@ -21,6 +21,7 @@ class _SRVFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.SRV, data: { "srv": [ diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart index 9be1cbb8b..9503544be 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/ssh_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class SSHForm extends NameFormStatefulWidget { - const SSHForm({super.key}); + const SSHForm({super.key, required super.name}); @override NameFormState createState() => _SSHFormState(); @@ -20,6 +20,7 @@ class _SSHFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.SSH, data: { "sshfp": [ diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart index bf2b1a33e..509e0b201 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/tls_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class TLSForm extends NameFormStatefulWidget { - const TLSForm({super.key}); + const TLSForm({super.key, required super.name}); @override NameFormState createState() => _TLSFormState(); @@ -18,6 +18,7 @@ class _TLSFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.TLS, data: { "map": { diff --git a/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart index 64f2111b7..bd331cd21 100644 --- a/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart +++ b/lib/pages/namecoin_names/add_dns_record/sub_widgets/txt_form.dart @@ -6,7 +6,7 @@ import '../../../../utilities/util.dart'; import '../name_form_interface.dart'; class TXTForm extends NameFormStatefulWidget { - const TXTForm({super.key}); + const TXTForm({super.key, required super.name}); @override NameFormState createState() => _TXTFormState(); @@ -18,6 +18,7 @@ class _TXTFormState extends NameFormState { @override DNSRecord buildRecord() { return DNSRecord( + name: widget.name, type: DNSRecordType.TXT, data: { "txt": [_valueController.text.trim()], diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index 73a3f5772..a43266637 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:namecoin/namecoin.dart'; @@ -20,6 +21,7 @@ import '../../models/namecoin_dns/dns_record_type.dart'; import '../../route_generator.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/show_loading.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../widgets/background.dart'; @@ -58,59 +60,76 @@ class _BuyDomainWidgetState extends ConsumerState { return DNSRecord.merge(_dnsRecords); } - bool _preRegLock = false; - Future _preRegister() async { - if (_preRegLock) return; - _preRegLock = true; - try { - final wallet = - ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; - final myAddress = await wallet.getCurrentReceivingAddress(); - if (myAddress == null) { - throw Exception("No receiving address found"); - } + String _getNameFormattedForInternal() { + String formattedName = widget.domainName; + if (!formattedName.startsWith("d/")) { + formattedName = "d/$formattedName"; + } + if (formattedName.endsWith(".bit")) { + formattedName.split(".bit").first; + } + return formattedName; + } - final value = _getFormattedDNSRecords(); + Future _preRegFuture() async { + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + final myAddress = await wallet.getCurrentReceivingAddress(); + if (myAddress == null) { + throw Exception("No receiving address found"); + } - Logging.instance.t("Formatted namecoin name value: $value"); + final value = _getFormattedDNSRecords(); - // get address private key for deterministic salt - final pk = await wallet.getPrivateKey(myAddress); + Logging.instance.t("Formatted namecoin name value: $value"); - String formattedName = widget.domainName; - if (!formattedName.startsWith("d/")) { - formattedName = "d/$formattedName"; - } - if (formattedName.endsWith(".bit")) { - formattedName.split(".bit").first; - } + // get address private key for deterministic salt + final pk = await wallet.getPrivateKey(myAddress); - final data = scriptNameNew(formattedName, pk.data); - - TxData txData = TxData( - opNameState: NameOpState( - name: formattedName, - saltHex: data.$2, - commitment: data.$3, - value: value, - nameScriptHex: data.$1, - type: OpName.nameNew, - outputPosition: -1, //currently unknown, updated later - ), - feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? - recipients: [ - ( - address: myAddress.value, - isChange: false, - amount: Amount( - rawValue: BigInt.from(kNameNewAmountSats), - fractionDigits: wallet.cryptoCurrency.fractionDigits, - ), + final formattedName = _getNameFormattedForInternal(); + + final data = await compute(_computeScriptNameNew, (formattedName, pk.data)); + + TxData txData = TxData( + opNameState: NameOpState( + name: formattedName, + saltHex: data.$2, + commitment: data.$3, + value: value, + nameScriptHex: data.$1, + type: OpName.nameNew, + outputPosition: -1, //currently unknown, updated later + ), + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: myAddress.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameNewAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, ), - ], - ); + ), + ], + ); + + txData = await wallet.prepareNameSend(txData: txData); + return txData; + } - txData = await wallet.prepareNameSend(txData: txData); + bool _preRegLock = false; + Future _preRegister() async { + if (_preRegLock) return; + _preRegLock = true; + try { + final txData = (await showLoading( + whileFuture: _preRegFuture(), + context: context, + message: "Preparing transaction...", + onException: (e) { + throw e; + }, + ))!; if (mounted) { if (Util.isDesktop) { @@ -121,7 +140,7 @@ class _BuyDomainWidgetState extends ConsumerState { width: 580, child: ConfirmNameTransactionView( txData: txData, - walletId: wallet.walletId, + walletId: widget.walletId, ), ), ), @@ -129,7 +148,7 @@ class _BuyDomainWidgetState extends ConsumerState { } else { await Navigator.of(context).pushNamed( ConfirmNameTransactionView.routeName, - arguments: (txData, wallet.walletId), + arguments: (txData, widget.walletId), ); } } @@ -145,7 +164,7 @@ class _BuyDomainWidgetState extends ConsumerState { await showDialog( context: context, builder: (_) => StackOkDialog( - title: "Add DNS record failed", + title: "Error", message: err, desktopPopRootNavigator: Util.isDesktop, maxWidth: Util.isDesktop ? 600 : null, @@ -191,23 +210,30 @@ class _BuyDomainWidgetState extends ConsumerState { ), DesktopDialogCloseButton( onPressedOverride: () { - Navigator.of(context, - rootNavigator: true) - .pop(); + Navigator.of( + context, + rootNavigator: true, + ).pop(); }, ), ], ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 32), - child: AddDnsStep1(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), ), ], ), ), ) - : const StackDialogBase( - child: AddDnsStep1(), + : StackDialogBase( + child: AddDnsStep1( + name: _getNameFormattedForInternal(), + ), ); }, ); @@ -247,6 +273,7 @@ class _BuyDomainWidgetState extends ConsumerState { builder: (child) { return Background( child: Scaffold( + backgroundColor: Colors.transparent, appBar: AppBar( leading: const AppBarBackButton(), titleSpacing: 0, @@ -256,9 +283,23 @@ class _BuyDomainWidgetState extends ConsumerState { overflow: TextOverflow.ellipsis, ), ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: child, + body: SafeArea( + child: LayoutBuilder( + builder: (ctx, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + ), ), ), ); @@ -279,7 +320,9 @@ class _BuyDomainWidgetState extends ConsumerState { height: Util.isDesktop ? 24 : 16, ), Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: Util.isDesktop + ? MainAxisAlignment.center + : MainAxisAlignment.start, children: [ Text( "Name registration will take approximately 2 to 4 hours.", @@ -363,15 +406,21 @@ class _BuyDomainWidgetState extends ConsumerState { ), ), SizedBox( - height: Util.isDesktop ? 16 : 8, + height: Util.isDesktop ? 24 : 16, ), - CustomTextButton( - text: _settingsHidden ? "More settings" : "Hide settings", - onTap: () { - setState(() { - _settingsHidden = !_settingsHidden; - }); - }, + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Row( + children: [child], + ), + child: CustomTextButton( + text: _settingsHidden ? "More settings" : "Hide settings", + onTap: () { + setState(() { + _settingsHidden = !_settingsHidden; + }); + }, + ), ), if (!_settingsHidden) SizedBox( @@ -395,38 +444,38 @@ class _BuyDomainWidgetState extends ConsumerState { ), ), if (!_settingsHidden) - if (_dnsRecords.isNotEmpty) - ListView( - shrinkWrap: true, + ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Expanded(child: child), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ ..._dnsRecords.map( (e) => DNSRecordCard( - key: Key(e.toString()), + key: ValueKey(e), record: e, onRemoveTapped: () => setState(() { _dnsRecords.remove(e); }), ), ), + SizedBox( + height: Util.isDesktop ? 16 : 8, + ), + SecondaryButton( + label: _dnsRecords.isEmpty + ? "Add DNS record" + : "Add another DNS record", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _addRecord, + ), ], ), - if (!_settingsHidden) - SizedBox( - height: Util.isDesktop ? 16 : 8, - ), - if (!_settingsHidden) - SecondaryButton( - label: _dnsRecords.isEmpty - ? "Add DNS record" - : "Add another DNS record", - // width: Util.isDesktop ? 160 : double.infinity, - buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: _addRecord, ), SizedBox( height: Util.isDesktop ? 24 : 16, ), - if (!Util.isDesktop) const Spacer(), + if (!Util.isDesktop && _settingsHidden) const Spacer(), PrimaryButton( label: "Buy", // width: Util.isDesktop ? 160 : double.infinity, @@ -442,6 +491,10 @@ class _BuyDomainWidgetState extends ConsumerState { } } +(String, String, String) _computeScriptNameNew((String, Uint8List) args) { + return scriptNameNew(args.$1, args.$2); +} + class DNSRecordCard extends StatelessWidget { const DNSRecordCard({ super.key, diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index 53e6017d2..f1e21861e 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -492,11 +492,13 @@ class _ConfirmNameTransactionViewState ], ), ), - if (widget.txData.note!.isNotEmpty) + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) const SizedBox( height: 12, ), - if (widget.txData.note!.isNotEmpty) + if (widget.txData.note != null && + widget.txData.note!.isNotEmpty) RoundedWhiteContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/pages/namecoin_names/namecoin_names_home_view.dart b/lib/pages/namecoin_names/namecoin_names_home_view.dart index 8cc1e5068..8970de7c6 100644 --- a/lib/pages/namecoin_names/namecoin_names_home_view.dart +++ b/lib/pages/namecoin_names/namecoin_names_home_view.dart @@ -101,63 +101,150 @@ class _NamecoinNamesHomeViewState extends ConsumerState { condition: !isDesktop, builder: (child) => SafeArea( child: Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + top: 16, + left: 16, + right: 16, + ), child: child, ), ), - child: Column( - crossAxisAlignment: - isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.only( - top: Util.isDesktop ? 24 : 16, - left: Util.isDesktop ? 24 : 16, - right: Util.isDesktop ? 24 : 16, - ), - child: SizedBox( - height: 48, - child: Toggle( - key: UniqueKey(), - onColor: Theme.of(context).extension()!.popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, - onText: "Buy domain", - offText: "Manage domains", - isOn: !_onManage, - onValueChanged: (value) { - setState(() { - _onManage = !value; - }); - }, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), + child: Util.isDesktop + ? Padding( + padding: const EdgeInsets.only( + top: 24, + left: 24, + right: 24, ), - ), - ), - Expanded( - child: Padding( - padding: EdgeInsets.all(Util.isDesktop ? 24 : 16), - child: IndexedStack( - index: _onManage ? 0 : 1, + child: Row( children: [ - BuyDomainOptionWidget( - walletId: widget.walletId, + SizedBox( + width: 460, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Buy domain", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox( + height: 14, + ), + Flexible( + child: BuyDomainOptionWidget( + walletId: widget.walletId, + ), + ), + ], + ), + ), + const SizedBox( + width: 24, ), - ManageDomainsOptionWidget( - walletId: widget.walletId, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + "Manage domains", + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ], + ), + const SizedBox( + height: 14, + ), + Flexible( + child: SingleChildScrollView( + child: ManageDomainsOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ], + ), ), ], ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Buy domain", + offText: "Manage domains", + isOn: !_onManage, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onManage = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: IndexedStack( + index: _onManage ? 0 : 1, + children: [ + BuyDomainOptionWidget( + walletId: widget.walletId, + ), + LayoutBuilder( + builder: (context, constraints) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: SingleChildScrollView( + child: IntrinsicHeight( + child: ManageDomainsOptionWidget( + walletId: widget.walletId, + ), + ), + ), + ); + }, + ), + ], + ), + ), + ], ), - ), - ], - ), ), ); } diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart index 14d902e88..966af3cad 100644 --- a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -182,7 +182,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), Container( height: 48, - width: 100, + width: Util.isDesktop ? 100 : 74, decoration: BoxDecoration( color: Theme.of(context) .extension()! @@ -282,8 +282,8 @@ class _NameCard extends ConsumerWidget { PrimaryButton( label: "Buy domain", enabled: isAvailable, - buttonHeight: ButtonHeight.m, - width: 140, + buttonHeight: Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, + width: Util.isDesktop ? 140 : 120, onPressed: () async { if (context.mounted) { if (Util.isDesktop) { diff --git a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart index a9261c2bb..37044728b 100644 --- a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart @@ -1,10 +1,15 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; import '../../../providers/db/main_db_provider.dart'; -import '../../../widgets/rounded_white_container.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import 'owned_name_card.dart'; class ManageDomainsOptionWidget extends ConsumerStatefulWidget { const ManageDomainsOptionWidget({ @@ -23,6 +28,7 @@ class _ManageDomainsWidgetState extends ConsumerState { @override Widget build(BuildContext context) { + final height = ref.watch(pWalletChainHeight(widget.walletId)); return StreamBuilder( stream: ref.watch( mainDBProvider.select( @@ -35,19 +41,37 @@ class _ManageDomainsWidgetState ), ), builder: (context, snapshot) { - List list = []; + List<(UTXO, OpNameData)> list = []; if (snapshot.hasData) { - list = snapshot.data!; + list = snapshot.data!.map((utxo) { + final data = jsonDecode(utxo.otherData!) as Map; + + final nameData = jsonDecode(data["nameOpData"] as String) as Map; + + return ( + utxo, + OpNameData(nameData.cast(), utxo.blockHeight ?? height) + ); + }).toList(growable: false); } - return ListView.separated( - itemCount: list.length, - itemBuilder: (context, index) => RoundedWhiteContainer( - child: Text(list[index].otherData!), - ), - separatorBuilder: (context, index) => const SizedBox( - height: 10, - ), + return Column( + children: [ + ...list.map( + (e) => Padding( + padding: const EdgeInsets.only( + bottom: 10, + ), + child: OwnedNameCard( + utxo: e.$1, + opNameData: e.$2, + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 14 : 6, + ), + ], ); }, ); diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart new file mode 100644 index 000000000..6c8ff25ea --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -0,0 +1,595 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/db/main_db_provider.dart'; +import '../../../providers/global/secure_store_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/background.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/simple_copy_button.dart'; +import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/rounded_container.dart'; +import '../../wallet_view/transaction_views/transaction_details_view.dart'; + +class NameDetailsView extends ConsumerStatefulWidget { + const NameDetailsView({ + super.key, + required this.utxoId, + required this.walletId, + }); + + static const routeName = "/namecoinNameDetails"; + + final Id utxoId; + final String walletId; + + @override + ConsumerState createState() => _ManageDomainsWidgetState(); +} + +class _ManageDomainsWidgetState extends ConsumerState { + late Stream streamUTXO; + UTXO? utxo; + OpNameData? opNameData; + + String? constructedName, value; + + Stream? streamLabel; + AddressLabel? label; + + void setUtxo(UTXO? utxo, int currentHeight) { + if (utxo != null) { + this.utxo = utxo; + final data = jsonDecode(utxo.otherData!) as Map; + + final nameData = jsonDecode(data["nameOpData"] as String) as Map; + opNameData = + OpNameData(nameData.cast(), utxo.blockHeight ?? currentHeight); + + _setName(); + } + } + + void _setName() { + try { + constructedName = opNameData!.constructedName; + value = opNameData!.value; + } catch (_) { + if (opNameData?.op == OpName.nameNew) { + ref + .read(secureStoreProvider) + .read( + key: nameSaltKeyBuilder( + utxo!.txid, + widget.walletId, + utxo!.vout, + ), + ) + .then((onValue) { + if (onValue != null) { + final data = (jsonDecode(onValue) as Map).cast(); + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = data["name"]!; + value = data["value"]!; + if (mounted) { + setState(() {}); + } + }); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = "UNKNOWN"; + value = ""; + if (mounted) { + setState(() {}); + } + }); + } + }); + } + } + } + + @override + void initState() { + super.initState(); + + setUtxo( + ref + .read(mainDBProvider) + .isar + .utxos + .where() + .idEqualTo(widget.utxoId) + .findFirstSync(), + ref.read(pWalletChainHeight(widget.walletId)), + ); + + _setName(); + + if (utxo?.address != null) { + label = ref.read(mainDBProvider).getAddressLabelSync( + widget.walletId, + utxo!.address!, + ); + + if (label != null) { + streamLabel = ref.read(mainDBProvider).watchAddressLabel(id: label!.id); + } + } + + streamUTXO = ref.read(mainDBProvider).watchUTXO(id: widget.utxoId); + } + + @override + Widget build(BuildContext context) { + final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); + + final isExpired = opNameData?.expired(currentHeight) == true; + final isSemiExpired = opNameData?.expired(currentHeight, true) == true; + + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + backgroundColor: Colors.transparent, + // Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Domain details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ); + }, + ), + ), + ), + ), + child: ConditionalParent( + condition: Util.isDesktop, + builder: (child) { + return SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "Domain details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 10, + ), + child: RoundedContainer( + padding: EdgeInsets.zero, + color: Colors.transparent, + borderColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: child, + ), + ), + ], + ), + ); + }, + child: StreamBuilder( + stream: streamUTXO, + builder: (context, snapshot) { + if (snapshot.hasData) { + setUtxo(snapshot.data!, currentHeight); + } + + return utxo == null + ? Center( + child: Text( + "Missing output. Was it used recently?", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorRed, + ), + ), + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // if (!isDesktop) + // const SizedBox( + // height: 10, + // ), + RoundedContainer( + padding: const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + // if (isDesktop) + // UTXOStatusIcon( + // blocked: utxo!.isBlocked, + // status: confirmed + // ? UTXOStatusIconStatus.confirmed + // : UTXOStatusIconStatus.unconfirmed, + // background: Theme.of(context) + // .extension()! + // .popupBG, + // selected: false, + // width: 32, + // height: 32, + // ), + // if (isDesktop) + // const SizedBox( + // width: 16, + // ), + + SelectableText( + constructedName ?? "", + style: STextStyles.pageTitleH2(context), + ), + ], + ), + SelectableText( + opNameData!.op.name, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Value", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + + // TODO: edit value + // SimpleEditButton( + // editValue: utxo!.name, + // editLabel: "label", + // onValueChanged: (newName) { + // MainDB.instance.putUTXO( + // utxo!.copyWith( + // name: newName, + // ), + // ); + // }, + // ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + value ?? "", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton( + data: utxo!.address!, + ) + : SimpleCopyButton( + data: utxo!.address!, + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + utxo!.address!, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + if (label != null && label!.value.isNotEmpty) + const _Div(), + if (label != null && label!.value.isNotEmpty) + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Address label", + style: + STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton( + data: label!.value, + ) + : SimpleCopyButton( + data: label!.value, + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + label!.value, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction ID", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + Util.isDesktop + ? IconCopyButton( + data: utxo!.txid, + ) + : SimpleCopyButton( + data: utxo!.txid, + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + utxo!.txid, + style: STextStyles.w500_14(context), + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Expiry", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 4, + ), + Row( + children: [ + SelectableText( + isExpired + ? "Expired" + : "${opNameData!.expiredBlockLeft(currentHeight)!}", + style: STextStyles.w500_14(context).copyWith( + color: isExpired + ? Theme.of(context) + .extension()! + .accentColorRed + : isSemiExpired + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + ), + ), + if (!isExpired) + Text( + " blocks left", + style: + STextStyles.w500_14(context).copyWith( + color: isExpired + ? Theme.of(context) + .extension()! + .accentColorRed + : isSemiExpired + ? Theme.of(context) + .extension()! + .accentColorYellow + : Theme.of(context) + .extension()! + .accentColorGreen, + ), + ), + ], + ), + ], + ), + ), + const _Div(), + RoundedContainer( + padding: Util.isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + color: Util.isDesktop + ? Colors.transparent + : Theme.of(context) + .extension()! + .popupBG, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Confirmations", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + const SizedBox( + height: 4, + ), + SelectableText( + "${utxo!.getConfirmations(currentHeight)}", + style: STextStyles.w500_14(context), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ); + } +} + +class _Div extends StatelessWidget { + const _Div({super.key}); + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return Container( + width: double.infinity, + height: 1.0, + color: Theme.of(context).extension()!.textFieldDefaultBG, + ); + } else { + return const SizedBox( + height: 12, + ); + } + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart new file mode 100644 index 000000000..79a08dcd6 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/isar_models.dart'; +import '../../../providers/global/secure_store_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/rounded_white_container.dart'; +import 'name_details.dart'; + +class OwnedNameCard extends ConsumerStatefulWidget { + const OwnedNameCard({ + super.key, + required this.opNameData, + required this.utxo, + }); + + final OpNameData opNameData; + final UTXO utxo; + + @override + ConsumerState createState() => _OwnedNameCardState(); +} + +class _OwnedNameCardState extends ConsumerState { + String? constructedName, value; + + (String, Color) getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + final remaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + false, + ); + final semiRemaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + true, + ); + + if (remaining == null) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "$remaining blocks remaining"; + if (semiRemaining == null) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + + return (message, color); + } + + bool _lock = false; + + Future _showDetails() async { + if (_lock) return; + _lock = true; + try { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: NameDetailsView( + utxoId: widget.utxo.id, + walletId: widget.utxo.walletId, + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + NameDetailsView.routeName, + arguments: ( + widget.utxo.id, + widget.utxo.walletId, + ), + ); + } + } finally { + _lock = false; + } + } + + void _setName() { + try { + constructedName = widget.opNameData.constructedName; + value = widget.opNameData.value; + } catch (_) { + if (widget.opNameData.op == OpName.nameNew) { + ref + .read(secureStoreProvider) + .read( + key: nameSaltKeyBuilder( + widget.utxo.txid, + widget.utxo.walletId, + widget.utxo.vout, + ), + ) + .then((onValue) { + if (onValue != null) { + final data = (jsonDecode(onValue) as Map).cast(); + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = data["name"]!; + value = data["value"]!; + if (mounted) { + setState(() {}); + } + }); + } else { + WidgetsBinding.instance.addPostFrameCallback((_) { + constructedName = "UNKNOWN"; + value = ""; + if (mounted) { + setState(() {}); + } + }); + } + }); + } + } + } + + @override + void initState() { + super.initState(); + _setName(); + } + + @override + Widget build(BuildContext context) { + final (message, color) = getExpiry( + ref.watch(pWalletChainHeight(widget.utxo.walletId)), + Theme.of(context).extension()!, + ); + + return RoundedWhiteContainer( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(constructedName ?? ""), + if (value != null) + const SizedBox( + height: 8, + ), + if (value != null) SelectableText(value!), + ], + ), + ), + Expanded( + flex: 2, + child: SelectableText( + message, + style: STextStyles.w500_12(context).copyWith( + color: color, + ), + ), + ), + PrimaryButton( + label: "Details", + buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, + onPressed: _showDetails, + ), + ], + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 3e51359ae..1539bdbeb 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -75,6 +75,7 @@ import 'pages/monkey/monkey_view.dart'; import 'pages/namecoin_names/buy_domain_view.dart'; import 'pages/namecoin_names/confirm_name_transaction_view.dart'; import 'pages/namecoin_names/namecoin_names_home_view.dart'; +import 'pages/namecoin_names/sub_widgets/name_details.dart'; import 'pages/notification_views/notifications_view.dart'; import 'pages/ordinals/ordinal_details_view.dart'; import 'pages/ordinals/ordinals_filter_view.dart'; @@ -719,6 +720,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case NameDetailsView.routeName: + if (args is (Id, String)) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => NameDetailsView( + walletId: args.$2, + utxoId: args.$1, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case PaynymClaimView.routeName: if (args is String) { return getRoute( diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 87dd63acc..ce6856baa 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -890,6 +890,12 @@ class NamecoinWallet final feeRateAmount = txData.feeRateAmount; final utxos = txData.utxos; + if (txData.note == null) { + txData = txData.copyWith( + note: "Name transaction ${txData.opNameState!.type.name}", + ); + } + final bool coinControl = utxos != null; if (customSatsPerVByte != null) { diff --git a/lib/widgets/desktop/desktop_scaffold.dart b/lib/widgets/desktop/desktop_scaffold.dart index 25b18963b..920c66306 100644 --- a/lib/widgets/desktop/desktop_scaffold.dart +++ b/lib/widgets/desktop/desktop_scaffold.dart @@ -9,6 +9,7 @@ */ import 'package:flutter/material.dart'; + import '../../themes/stack_colors.dart'; import '../background.dart'; @@ -70,8 +71,7 @@ class MasterScaffold extends StatelessWidget { } else { return Background( child: Scaffold( - backgroundColor: background ?? - Theme.of(context).extension()!.background, + backgroundColor: background ?? Colors.transparent, appBar: appBar as PreferredSizeWidget?, body: body, ), From 4695210a058e1f65b38eb9be1b5581c666ae7828 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Feb 2025 17:48:44 -0600 Subject: [PATCH 23/29] domain name input length restriction --- .../sub_widgets/buy_domain_option_widget.dart | 180 +++++++++++------- lib/utilities/text_formatters.dart | 63 ++++++ 2 files changed, 179 insertions(+), 64 deletions(-) create mode 100644 lib/utilities/text_formatters.dart diff --git a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart index 966af3cad..1040dda4f 100644 --- a/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/buy_domain_option_widget.dart @@ -3,13 +3,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:namecoin/namecoin.dart'; import '../../../providers/providers.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/assets.dart'; import '../../../utilities/constants.dart'; +import '../../../utilities/extensions/impl/string.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/show_loading.dart'; +import '../../../utilities/text_formatters.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.dart'; @@ -31,6 +34,8 @@ class BuyDomainOptionWidget extends ConsumerStatefulWidget { } class _BuyDomainWidgetState extends ConsumerState { + static const kMaxByteLength = nameMaxLength - 2; // subtract length of "d/" + final _nameController = TextEditingController(); final _nameFieldFocus = FocusNode(); @@ -77,11 +82,17 @@ class _BuyDomainWidgetState extends ConsumerState { } catch (e, s) { Logging.instance.e("_lookup failed", error: e, stackTrace: s); + String? err; + if (e.toString().contains("Contains invalid characters")) { + err = "Contains invalid characters"; + } + if (mounted) { await showDialog( context: context, builder: (_) => StackOkDialog( title: "Name lookup failed", + message: err, desktopPopRootNavigator: Util.isDesktop, maxWidth: Util.isDesktop ? 600 : null, ), @@ -111,6 +122,7 @@ class _BuyDomainWidgetState extends ConsumerState { @override Widget build(BuildContext context) { + final double dotBitBoxLength = Util.isDesktop ? 100 : 74; return Column( crossAxisAlignment: Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, @@ -140,6 +152,11 @@ class _BuyDomainWidgetState extends ConsumerState { children: [ Expanded( child: TextField( + inputFormatters: [ + Utf8ByteLengthLimitingTextInputFormatter( + kMaxByteLength, + ), + ], textInputAction: TextInputAction.search, focusNode: _nameFieldFocus, controller: _nameController, @@ -170,7 +187,7 @@ class _BuyDomainWidgetState extends ConsumerState { _lookup(); } }, - onChanged: (_) { + onChanged: (value) { // trigger look up button enabled/disabled state change setState(() {}); }, @@ -182,7 +199,7 @@ class _BuyDomainWidgetState extends ConsumerState { ), Container( height: 48, - width: Util.isDesktop ? 100 : 74, + width: dotBitBoxLength, decoration: BoxDecoration( color: Theme.of(context) .extension()! @@ -209,6 +226,31 @@ class _BuyDomainWidgetState extends ConsumerState { ], ), ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(right: dotBitBoxLength), + child: Builder( + builder: (context) { + final length = + _nameController.text.toUint8ListFromUtf8.lengthInBytes; + return Text( + "$length/$kMaxByteLength", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ), + ], + ), SizedBox( height: Util.isDesktop ? 24 : 16, ), @@ -263,78 +305,88 @@ class _NameCard extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${formattedName.substring(2)}.bit", - style: style, - ), - Text( - availability, - style: style.copyWith( - color: color, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${formattedName.substring(2)}.bit", + style: style, ), - ), - ], + const SizedBox( + height: 4, + ), + Text( + availability, + style: style.copyWith( + color: color, + ), + ), + ], + ), ), - PrimaryButton( - label: "Buy domain", - enabled: isAvailable, - buttonHeight: Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, - width: Util.isDesktop ? 140 : 120, - onPressed: () async { - if (context.mounted) { - if (Util.isDesktop) { - await showDialog( - context: context, - builder: (context) => SDialog( - child: SizedBox( - width: 580, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, + Column( + children: [ + PrimaryButton( + label: "Buy domain", + enabled: isAvailable, + buttonHeight: + Util.isDesktop ? ButtonHeight.m : ButtonHeight.l, + width: Util.isDesktop ? 140 : 120, + onPressed: () async { + if (context.mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: Column( children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Buy domain", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), Padding( - padding: const EdgeInsets.only( - left: 32, + padding: const EdgeInsets.symmetric( + horizontal: 32, ), - child: Text( - "Buy domain", - style: STextStyles.desktopH3(context), + child: BuyDomainView( + walletId: walletId, + domainName: formattedName, ), ), - const DesktopDialogCloseButton(), ], ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32, - ), - child: BuyDomainView( - walletId: walletId, - domainName: formattedName, - ), - ), - ], + ), ), - ), - ), - ); - } else { - await Navigator.of(context).pushNamed( - BuyDomainView.routeName, - arguments: ( - walletId: walletId, - domainName: formattedName - ), - ); - } - } - }, + ); + } else { + await Navigator.of(context).pushNamed( + BuyDomainView.routeName, + arguments: ( + walletId: walletId, + domainName: formattedName + ), + ); + } + } + }, + ), + ], ), ], ), diff --git a/lib/utilities/text_formatters.dart b/lib/utilities/text_formatters.dart new file mode 100644 index 000000000..70a96231d --- /dev/null +++ b/lib/utilities/text_formatters.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:flutter/services.dart'; + +class Utf8ByteLengthLimitingTextInputFormatter extends TextInputFormatter { + Utf8ByteLengthLimitingTextInputFormatter( + this.maxBytes, + ) : assert(maxBytes == -1 || maxBytes > 0); + + final int maxBytes; + + static TextEditingValue truncate(TextEditingValue value, int maxBytes) { + final String text = value.text; + final encoded = utf8.encode(text); + + if (encoded.length <= maxBytes) { + return value; + } + + int validLength = maxBytes; + while (validLength > 0 && (encoded[validLength] & 0xC0) == 0x80) { + validLength--; + } + + final truncated = utf8.decode(encoded.sublist(0, validLength)); + + return TextEditingValue( + text: truncated, + selection: value.selection.copyWith( + baseOffset: math.min(value.selection.start, truncated.length), + extentOffset: math.min(value.selection.end, truncated.length), + ), + composing: !value.composing.isCollapsed && + truncated.length > value.composing.start + ? TextRange( + start: value.composing.start, + end: math.min(value.composing.end, truncated.length), + ) + : TextRange.empty, + ); + } + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + if (maxBytes == -1 || + utf8.encode(newValue.text).lengthInBytes <= maxBytes) { + return newValue; + } + + assert(maxBytes > 0); + + if (utf8.encode(oldValue.text).lengthInBytes == maxBytes && + oldValue.selection.isCollapsed) { + return oldValue; + } + + return truncate(newValue, maxBytes); + } +} From 580898865d92eef8b7fc52beeedefa1d6bf6537e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Feb 2025 17:49:25 -0600 Subject: [PATCH 24/29] clean up domain names management ui somewhat --- .../manage_domains_option_widget.dart | 24 ++++++ .../sub_widgets/name_details.dart | 2 +- .../sub_widgets/owned_name_card.dart | 83 ++++++++++++++----- 3 files changed, 86 insertions(+), 23 deletions(-) diff --git a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart index 37044728b..2d696d64b 100644 --- a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart @@ -26,6 +26,25 @@ class ManageDomainsOptionWidget extends ConsumerStatefulWidget { class _ManageDomainsWidgetState extends ConsumerState { + double _tempWidth = 0; + double? _width; + int _count = 0; + + void _sillyHack(double value, int length) { + if (value > _tempWidth) _tempWidth = value; + _count++; + if (_count == length) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _width = _tempWidth; + _tempWidth = 0; + }); + } + }); + } + } + @override Widget build(BuildContext context) { final height = ref.watch(pWalletChainHeight(widget.walletId)); @@ -65,6 +84,11 @@ class _ManageDomainsWidgetState child: OwnedNameCard( utxo: e.$1, opNameData: e.$2, + firstColWidth: _width, + calculatedFirstColWidth: (value) => _sillyHack( + value, + list.length, + ), ), ), ), diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index 6c8ff25ea..33bd9a30c 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -513,7 +513,7 @@ class _ManageDomainsWidgetState extends ConsumerState { ), if (!isExpired) Text( - " blocks left", + " blocks remaining", style: STextStyles.w500_14(context).copyWith( color: isExpired diff --git a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart index 79a08dcd6..74b6577c2 100644 --- a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart +++ b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart @@ -11,6 +11,7 @@ import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/conditional_parent.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_white_container.dart'; @@ -21,11 +22,16 @@ class OwnedNameCard extends ConsumerStatefulWidget { super.key, required this.opNameData, required this.utxo, + this.firstColWidth, + this.calculatedFirstColWidth, }); final OpNameData opNameData; final UTXO utxo; + final double? firstColWidth; + final void Function(double)? calculatedFirstColWidth; + @override ConsumerState createState() => _OwnedNameCardState(); } @@ -33,7 +39,7 @@ class OwnedNameCard extends ConsumerStatefulWidget { class _OwnedNameCardState extends ConsumerState { String? constructedName, value; - (String, Color) getExpiry(int currentChainHeight, StackColors theme) { + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { final String message; final Color color; @@ -50,7 +56,7 @@ class _OwnedNameCardState extends ConsumerState { color = theme.accentColorRed; message = "Expired"; } else { - message = "$remaining blocks remaining"; + message = "Expires in $remaining blocks"; if (semiRemaining == null) { color = theme.accentColorYellow; } else { @@ -136,9 +142,13 @@ class _OwnedNameCardState extends ConsumerState { _setName(); } + double _callbackWidth = 0; + @override Widget build(BuildContext context) { - final (message, color) = getExpiry( + debugPrint("BUILD: $runtimeType"); + + final (message, color) = _getExpiry( ref.watch(pWalletChainHeight(widget.utxo.walletId)), Theme.of(context).extension()!, ); @@ -148,29 +158,58 @@ class _OwnedNameCardState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - flex: 5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText(constructedName ?? ""), - if (value != null) - const SizedBox( - height: 8, - ), - if (value != null) SelectableText(value!), - ], + ConditionalParent( + condition: widget.firstColWidth != null && Util.isDesktop, + builder: (child) => ConstrainedBox( + constraints: BoxConstraints(maxWidth: widget.firstColWidth!), + child: child, ), - ), - Expanded( - flex: 2, - child: SelectableText( - message, - style: STextStyles.w500_12(context).copyWith( - color: color, + child: ConditionalParent( + condition: widget.firstColWidth == null && Util.isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) { + if (widget.firstColWidth == null && + _callbackWidth != constraints.maxWidth) { + _callbackWidth = constraints.maxWidth; + widget.calculatedFirstColWidth?.call(_callbackWidth); + } + return ConstrainedBox( + constraints: BoxConstraints(maxWidth: constraints.maxWidth), + child: child, + ); + }, + ), + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(constructedName ?? ""), + const SizedBox( + height: 8, + ), + SelectableText( + message, + style: STextStyles.w500_12(context).copyWith( + color: color, + ), + ), + ], + ), ), ), ), + if (Util.isDesktop) + Expanded( + child: SelectableText( + value ?? "", + style: STextStyles.w500_12(context), + ), + ), + if (Util.isDesktop) + const SizedBox( + width: 12, + ), PrimaryButton( label: "Details", buttonHeight: Util.isDesktop ? ButtonHeight.xs : ButtonHeight.l, From ac1f991928cbb123013a56f4f0a4b1a93b5a211b Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Feb 2025 10:41:11 -0600 Subject: [PATCH 25/29] WIP: manage domain ui --- lib/pages/namecoin_names/buy_domain_view.dart | 10 +- .../namecoin_names/manage_domain_view.dart | 109 +++++ .../sub_widgets/name_details.dart | 126 ++++-- .../sub_widgets/transfer_option_widget.dart | 424 ++++++++++++++++++ .../sub_widgets/update_option_widget.dart | 105 +++++ lib/route_generator.dart | 16 + lib/wallets/models/name_op_state.dart | 8 +- lib/wallets/wallet/impl/namecoin_wallet.dart | 16 +- 8 files changed, 771 insertions(+), 43 deletions(-) create mode 100644 lib/pages/namecoin_names/manage_domain_view.dart create mode 100644 lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart create mode 100644 lib/pages/namecoin_names/sub_widgets/update_option_widget.dart diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index a43266637..19f6dafda 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -155,12 +155,12 @@ class _BuyDomainWidgetState extends ConsumerState { } catch (e, s) { Logging.instance.e("_preRegister failed", error: e, stackTrace: s); - String err = e.toString(); - if (err.startsWith("Exception: ")) { - err = err.replaceFirst("Exception: ", ""); - } - if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + await showDialog( context: context, builder: (_) => StackOkDialog( diff --git a/lib/pages/namecoin_names/manage_domain_view.dart b/lib/pages/namecoin_names/manage_domain_view.dart new file mode 100644 index 000000000..ba17957be --- /dev/null +++ b/lib/pages/namecoin_names/manage_domain_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import '../../models/isar/models/blockchain_data/utxo.dart'; +import '../../themes/stack_colors.dart'; +import '../../utilities/constants.dart'; +import '../../utilities/text_styles.dart'; +import '../../utilities/util.dart'; +import '../../widgets/background.dart'; +import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../widgets/toggle.dart'; +import 'sub_widgets/transfer_option_widget.dart'; +import 'sub_widgets/update_option_widget.dart'; + +class ManageDomainView extends StatefulWidget { + const ManageDomainView({ + super.key, + required this.walletId, + required this.utxo, + }); + + final String walletId; + final UTXO utxo; + + static const routeName = "/manageDomainView"; + + @override + State createState() => _ManageDomainViewState(); +} + +class _ManageDomainViewState extends State { + bool _onTransfer = true; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + "Manage domain", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: child, + ), + ), + ), + ); + }, + child: Column( + children: [ + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Transfer", + offText: "Update", + isOn: !_onTransfer, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onTransfer = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: IndexedStack( + index: _onTransfer ? 0 : 1, + children: [ + TransferOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + UpdateOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index 33bd9a30c..48114aa9a 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -16,10 +16,13 @@ import '../../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../../widgets/background.dart'; import '../../../widgets/conditional_parent.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; +import '../../../widgets/custom_buttons/blue_text_button.dart'; import '../../../widgets/custom_buttons/simple_copy_button.dart'; import '../../../widgets/desktop/desktop_dialog_close_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_container.dart'; import '../../wallet_view/transaction_views/transaction_details_view.dart'; +import '../manage_domain_view.dart'; class NameDetailsView extends ConsumerStatefulWidget { const NameDetailsView({ @@ -150,6 +153,27 @@ class _ManageDomainsWidgetState extends ConsumerState { "Domain details", style: STextStyles.navBarTitle(context), ), + actions: utxo == null + ? null + : [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 10, + ), + child: CustomTextButton( + key: const Key("addAddressBookEntryFavoriteButtonKey"), + text: "Manage", + onTap: () { + Navigator.of(context).pushNamed( + ManageDomainView.routeName, + arguments: (walletId: widget.walletId, utxo: utxo!), + ); + }, + ), + ), + ], ), body: SafeArea( child: LayoutBuilder( @@ -247,36 +271,79 @@ class _ManageDomainsWidgetState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // if (isDesktop) - // UTXOStatusIcon( - // blocked: utxo!.isBlocked, - // status: confirmed - // ? UTXOStatusIconStatus.confirmed - // : UTXOStatusIconStatus.unconfirmed, - // background: Theme.of(context) - // .extension()! - // .popupBG, - // selected: false, - // width: 32, - // height: 32, - // ), - // if (isDesktop) - // const SizedBox( - // width: 16, - // ), - SelectableText( constructedName ?? "", style: STextStyles.pageTitleH2(context), ), + if (Util.isDesktop) + SelectableText( + opNameData!.op.name, + style: STextStyles.w500_14(context), + ), ], ), - SelectableText( - opNameData!.op.name, - style: STextStyles.w500_14(context), - ), + if (Util.isDesktop && utxo != null) + CustomTextButton( + text: "Manage", + onTap: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment + .spaceBetween, + children: [ + Padding( + padding: + const EdgeInsets.only( + left: 32, + ), + child: Text( + "Manage domain", + style: + STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: IntrinsicHeight( + child: ManageDomainView( + walletId: widget.walletId, + utxo: utxo!, + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + if (!Util.isDesktop) + SelectableText( + opNameData!.op.name, + style: STextStyles.w500_14(context), + ), ], ), ), @@ -305,19 +372,6 @@ class _ManageDomainsWidgetState extends ConsumerState { .textSubtitle1, ), ), - - // TODO: edit value - // SimpleEditButton( - // editValue: utxo!.name, - // editLabel: "label", - // onValueChanged: (newName) { - // MainDB.instance.putUTXO( - // utxo!.copyWith( - // name: newName, - // ), - // ); - // }, - // ), ], ), const SizedBox( diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart new file mode 100644 index 000000000..1bd9c9cde --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -0,0 +1,424 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/providers.dart'; +import '../../../utilities/address_utils.dart'; +import '../../../utilities/amount/amount.dart'; +import '../../../utilities/barcode_scanner_interface.dart'; +import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/constants.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/text_styles.dart'; +import '../../../utilities/util.dart'; +import '../../../wallets/isar/providers/wallet_info_provider.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; +import '../../../widgets/icon_widgets/addressbook_icon.dart'; +import '../../../widgets/icon_widgets/clipboard_icon.dart'; +import '../../../widgets/icon_widgets/qrcode_icon.dart'; +import '../../../widgets/icon_widgets/x_icon.dart'; +import '../../../widgets/stack_dialog.dart'; +import '../../../widgets/stack_text_field.dart'; +import '../../../widgets/textfield_icon_button.dart'; +import '../../address_book_views/address_book_view.dart'; +import '../../send_view/sub_widgets/building_transaction_dialog.dart'; +import '../confirm_name_transaction_view.dart'; + +class TransferOptionWidget extends ConsumerStatefulWidget { + const TransferOptionWidget({ + super.key, + required this.walletId, + required this.utxo, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }); + + final String walletId; + final UTXO utxo; + + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => + _TransferOptionWidgetState(); +} + +class _TransferOptionWidgetState extends ConsumerState { + late final String walletId; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + + String? _address; + + bool _previewLock = false; + Future _preview() async { + if (_previewLock) return; + _previewLock = true; + + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + try { + final wallet = ref.read(pWallets).getWallet(walletId) as NamecoinWallet; + + bool wasCancelled = false; + + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final txDataFuture = wallet.prepareNameSend( + txData: TxData( + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: _address!, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + note: "Transfer domain name", + opNameState: NameOpState( + name: "", + saltHex: "", + commitment: "", + value: "", + nameScriptHex: "", + type: OpName.nameUpdate, + output: widget.utxo, + outputPosition: -1, //currently unknown, updated later + ), + // satsPerVByte: isCustomFee ? customFeeRate : null, + // utxos: (wallet is CoinControlInterface && + // coinControlEnabled && + // selectedUTXOs.isNotEmpty) + // ? selectedUTXOs + // : null, + ), + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = results.first as TxData; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } + } catch (e, s) { + Logging.instance.e("_preview failed", error: e, stackTrace: s); + + if (mounted) { + String err = e.toString(); + if (err.startsWith("Exception: ")) { + err = err.replaceFirst("Exception: ", ""); + } + + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Error", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _previewLock = false; + } + } + + bool _enableButton = false; + + void _setValidAddressProviders(String? address) { + _enableButton = ref + .read(pWallets) + .getWallet(walletId) + .cryptoCurrency + .validateAddress(address ?? ""); + if (mounted) { + setState(() {}); + } + } + + Future _scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + final coin = ref.read(pWalletCoin(walletId)); + + Logging.instance.d("qrResult content: ${qrResult.rawContent}"); + + final paymentData = AddressUtils.parsePaymentUri( + qrResult.rawContent, + logging: Logging.instance, + ); + + if (paymentData != null && + paymentData.coin?.uriScheme == coin.uriScheme) { + // auto fill address + _address = paymentData.address.trim(); + _addressController.text = _address!; + + _setValidAddressProviders(_address); + + // now check for non standard encoded basic address + } else { + _address = qrResult.rawContent.split("\n").first.trim(); + _addressController.text = _address ?? ""; + + _setValidAddressProviders(_address); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.e( + "Failed to get camera permissions while trying to scan qr code in" + " $runtimeType", + error: e, + stackTrace: s, + ); + } + } + + @override + void initState() { + super.initState(); + walletId = widget.walletId; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + _addressController = TextEditingController(); + _addressFocusNode = FocusNode(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _addressFocusNode.requestFocus(); + } + }); + } + + @override + void dispose() { + _addressController.dispose(); + _addressFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("nameTransferViewAddressFieldKey"), + controller: _addressController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue.trim(); + _setValidAddressProviders(_address); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${ref.watch(pWalletCoin(walletId)).ticker} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _addressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressController.text.isNotEmpty + ? TextFieldIconButton( + semanticsLabel: + "Clear Button. Clears The Address Field Input.", + key: const Key( + "nameTransferClearAddressFieldButtonKey", + ), + onTap: () { + _addressController.text = ""; + _address = ""; + _setValidAddressProviders( + _address, + ); + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + semanticsLabel: + "Paste Button. Pastes From Clipboard To Address Field Input.", + key: const Key( + "nameTransferPasteAddressFieldButtonKey", + ), + onTap: () async { + final ClipboardData? data = + await clipboard.getData( + Clipboard.kTextPlain, + ); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, + content.indexOf( + "\n", + ), + ); + } + + _addressController.text = content.trim(); + _address = content.trim(); + + _setValidAddressProviders( + _address, + ); + } + }, + child: _addressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Address Book Button. Opens Address Book For Address Field.", + key: const Key( + "nameTransferAddressBookButtonKey", + ), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: ref.read(pWalletCoin(walletId)), + ); + }, + child: const AddressBookIcon(), + ), + if (_addressController.text.isEmpty) + TextFieldIconButton( + semanticsLabel: + "Scan QR Button. Opens Camera For Scanning QR Code.", + key: const Key( + "nameTransferScanQrButtonKey", + ), + onTap: _scanQr, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + // if (!Util.isDesktop) const Spacer(), + PrimaryButton( + label: "Transfer", + enabled: _enableButton, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _preview, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + ], + ); + } +} diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart new file mode 100644 index 000000000..64ffe7423 --- /dev/null +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../utilities/barcode_scanner_interface.dart'; +import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/logger.dart'; +import '../../../utilities/util.dart'; +import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/stack_dialog.dart'; + +class UpdateOptionWidget extends ConsumerStatefulWidget { + const UpdateOptionWidget({ + super.key, + required this.walletId, + required this.utxo, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }); + + final String walletId; + final UTXO utxo; + + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => _BuyDomainWidgetState(); +} + +class _BuyDomainWidgetState extends ConsumerState { + final _nameController = TextEditingController(); + final _nameFieldFocus = FocusNode(); + + bool _lookupLock = false; + Future _lookup() async { + if (_lookupLock) return; + _lookupLock = true; + try {} catch (e, s) { + Logging.instance.e("_lookup failed", error: e, stackTrace: s); + + String? err; + if (e.toString().contains("Contains invalid characters")) { + err = "Contains invalid characters"; + } + + if (mounted) { + await showDialog( + context: context, + builder: (_) => StackOkDialog( + title: "Name lookup failed", + message: err, + desktopPopRootNavigator: Util.isDesktop, + maxWidth: Util.isDesktop ? 600 : null, + ), + ); + } + } finally { + _lookupLock = false; + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _nameFieldFocus.requestFocus(); + } + }); + } + + @override + void dispose() { + _nameController.dispose(); + _nameFieldFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + children: [ + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + SecondaryButton( + label: "Update", + enabled: _nameController.text.isNotEmpty, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _lookup, + ), + SizedBox( + height: Util.isDesktop ? 24 : 16, + ), + ], + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 1539bdbeb..6695a53b8 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -74,6 +74,7 @@ import 'pages/manage_favorites_view/manage_favorites_view.dart'; import 'pages/monkey/monkey_view.dart'; import 'pages/namecoin_names/buy_domain_view.dart'; import 'pages/namecoin_names/confirm_name_transaction_view.dart'; +import 'pages/namecoin_names/manage_domain_view.dart'; import 'pages/namecoin_names/namecoin_names_home_view.dart'; import 'pages/namecoin_names/sub_widgets/name_details.dart'; import 'pages/notification_views/notifications_view.dart'; @@ -805,6 +806,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ManageDomainView.routeName: + if (args is ({String walletId, UTXO utxo})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ManageDomainView( + walletId: args.walletId, + utxo: args.utxo, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case FusionProgressView.routeName: if (args is String) { return getRoute( diff --git a/lib/wallets/models/name_op_state.dart b/lib/wallets/models/name_op_state.dart index 577663f12..5390db018 100644 --- a/lib/wallets/models/name_op_state.dart +++ b/lib/wallets/models/name_op_state.dart @@ -1,5 +1,7 @@ import 'package:namecoin/namecoin.dart'; +import '../../models/isar/models/blockchain_data/utxo.dart'; + class NameOpState { final String name; final OpName type; @@ -8,6 +10,7 @@ class NameOpState { final String value; final String nameScriptHex; final int outputPosition; + final UTXO? output; NameOpState({ required this.name, @@ -17,6 +20,7 @@ class NameOpState { required this.value, required this.nameScriptHex, required this.outputPosition, + this.output, }); NameOpState copyWith({ @@ -36,6 +40,7 @@ class NameOpState { value: value ?? this.value, nameScriptHex: nameScriptHex ?? this.nameScriptHex, outputPosition: outputPosition ?? this.outputPosition, + output: output, ); } @@ -48,6 +53,7 @@ class NameOpState { "commitment: $commitment, " "value: $value, " "nameScriptHex: $nameScriptHex, " - "outputPosition: $outputPosition)"; + "outputPosition: $outputPosition, " + "output: $output)"; } } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index ce6856baa..0f863cc14 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -982,6 +982,11 @@ class NamecoinWallet throw Exception("Coin control used where utxos is null!"); } + if (txData.opNameState!.type == OpName.nameUpdate && + txData.opNameState!.output == null) { + throw Exception("Missing name output to update"); + } + final recipientAddress = txData.recipients!.first.address; final satoshiAmountToSend = txData.amount!.raw; final int? satsPerVByte = txData.satsPerVByte; @@ -1005,6 +1010,12 @@ class NamecoinWallet final List availableOutputs = utxos ?? await mainDB.getUTXOs(walletId).findAll(); + + if (txData.opNameState!.type == OpName.nameUpdate) { + // name output is added later + availableOutputs.removeWhere((e) => e == txData.opNameState!.output!); + } + final currentChainHeight = await chainHeight; final canCPFP = this is CpfpInterface && coinControl; @@ -1138,7 +1149,10 @@ class NamecoinWallet BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; - final List utxoObjectsToUse = []; + final List utxoObjectsToUse = [ + if (txData.opNameState!.type == OpName.nameUpdate) + txData.opNameState!.output!, + ]; if (!coinControl) { for (int i = 0; From 4f15af3c9c053894cd4e661fea56d82bda0684ab Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Feb 2025 12:05:58 -0600 Subject: [PATCH 26/29] name tx note tweaks --- lib/pages/namecoin_names/buy_domain_view.dart | 1 + lib/wallets/wallet/impl/namecoin_wallet.dart | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lib/pages/namecoin_names/buy_domain_view.dart b/lib/pages/namecoin_names/buy_domain_view.dart index 19f6dafda..d0dddda3e 100644 --- a/lib/pages/namecoin_names/buy_domain_view.dart +++ b/lib/pages/namecoin_names/buy_domain_view.dart @@ -100,6 +100,7 @@ class _BuyDomainWidgetState extends ConsumerState { type: OpName.nameNew, outputPosition: -1, //currently unknown, updated later ), + note: "Reserve ${widget.domainName.substring(2)}.bit", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ ( diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 0f863cc14..10c804022 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -580,6 +580,12 @@ class NamecoinWallet data.salt, ); + String noteName = + data.name.startsWith("d/") ? data.name.substring(2) : data.name; + if (!noteName.endsWith(".bit")) { + noteName += ".bit"; + } + TxData txData = TxData( utxos: {utxo}, opNameState: NameOpState( @@ -591,6 +597,7 @@ class NamecoinWallet type: OpName.nameFirstUpdate, outputPosition: -1, //currently unknown, updated later ), + note: "Purchase $noteName", feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? recipients: [ ( From f82ca2b478c32cbe82da9130f4789dc3fdf4e0d1 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Feb 2025 15:17:49 -0600 Subject: [PATCH 27/29] mobile/desktop name ui clean up and transfer name functionality --- .../confirm_name_transaction_view.dart | 8 +- .../namecoin_names/manage_domain_view.dart | 123 ++++++------- .../manage_domains_option_widget.dart | 1 + .../sub_widgets/name_details.dart | 173 ++++++++++++------ .../sub_widgets/transfer_option_widget.dart | 128 +++++++++---- lib/wallets/wallet/impl/namecoin_wallet.dart | 136 +++----------- 6 files changed, 304 insertions(+), 265 deletions(-) diff --git a/lib/pages/namecoin_names/confirm_name_transaction_view.dart b/lib/pages/namecoin_names/confirm_name_transaction_view.dart index f1e21861e..ec0fc926a 100644 --- a/lib/pages/namecoin_names/confirm_name_transaction_view.dart +++ b/lib/pages/namecoin_names/confirm_name_transaction_view.dart @@ -15,6 +15,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:namecoin/namecoin.dart'; import '../../models/isar/models/transaction_note.dart'; import '../../notifications/show_flush_bar.dart'; @@ -160,8 +161,13 @@ class _ConfirmNameTransactionViewState Navigator.of(context, rootNavigator: Util.isDesktop).pop(); // pop confirm send view Navigator.of(context, rootNavigator: Util.isDesktop).pop(); - // pop buy popup //TODO test on mobile + // pop buy popup Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + + // pop name details view + if (txData.opNameState!.type == OpName.nameUpdate) { + Navigator.of(context, rootNavigator: Util.isDesktop).pop(); + } } } catch (e, s) { const niceError = "Broadcast name transaction failed"; diff --git a/lib/pages/namecoin_names/manage_domain_view.dart b/lib/pages/namecoin_names/manage_domain_view.dart index ba17957be..6d2679d60 100644 --- a/lib/pages/namecoin_names/manage_domain_view.dart +++ b/lib/pages/namecoin_names/manage_domain_view.dart @@ -4,9 +4,7 @@ import '../../models/isar/models/blockchain_data/utxo.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/constants.dart'; import '../../utilities/text_styles.dart'; -import '../../utilities/util.dart'; import '../../widgets/background.dart'; -import '../../widgets/conditional_parent.dart'; import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/toggle.dart'; import 'sub_widgets/transfer_option_widget.dart'; @@ -33,76 +31,71 @@ class _ManageDomainViewState extends State { @override Widget build(BuildContext context) { - return ConditionalParent( - condition: !Util.isDesktop, - builder: (child) { - return Background( - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: const AppBarBackButton(), - titleSpacing: 0, - title: Text( - "Manage domain", - style: STextStyles.navBarTitle(context), - overflow: TextOverflow.ellipsis, - ), - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: child, - ), - ), + return Background( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: const AppBarBackButton(), + titleSpacing: 0, + title: Text( + "Manage domain", + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, ), - ); - }, - child: Column( - children: [ - SizedBox( - height: 48, - child: Toggle( - key: UniqueKey(), - onColor: Theme.of(context).extension()!.popupBG, - offColor: Theme.of(context) - .extension()! - .textFieldDefaultBG, - onText: "Transfer", - offText: "Update", - isOn: !_onTransfer, - onValueChanged: (value) { - FocusManager.instance.primaryFocus?.unfocus(); - setState(() { - _onTransfer = !value; - }); - }, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - Expanded( - child: IndexedStack( - index: _onTransfer ? 0 : 1, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( children: [ - TransferOptionWidget( - walletId: widget.walletId, - utxo: widget.utxo, + SizedBox( + height: 48, + child: Toggle( + key: UniqueKey(), + onColor: + Theme.of(context).extension()!.popupBG, + offColor: Theme.of(context) + .extension()! + .textFieldDefaultBG, + onText: "Transfer", + offText: "Update", + isOn: !_onTransfer, + onValueChanged: (value) { + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { + _onTransfer = !value; + }); + }, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + ), + ), + const SizedBox( + height: 16, ), - UpdateOptionWidget( - walletId: widget.walletId, - utxo: widget.utxo, + Expanded( + child: IndexedStack( + index: _onTransfer ? 0 : 1, + children: [ + TransferOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + UpdateOptionWidget( + walletId: widget.walletId, + utxo: widget.utxo, + ), + ], + ), ), ], ), ), - ], + ), ), ); } diff --git a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart index 2d696d64b..f4a7df875 100644 --- a/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/manage_domains_option_widget.dart @@ -82,6 +82,7 @@ class _ManageDomainsWidgetState bottom: 10, ), child: OwnedNameCard( + key: ValueKey(e), utxo: e.$1, opNameData: e.$2, firstColWidth: _width, diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index 48114aa9a..fbcbb01f3 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -19,10 +19,13 @@ 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/desktop_dialog_close_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/rounded_container.dart'; import '../../wallet_view/transaction_views/transaction_details_view.dart'; import '../manage_domain_view.dart'; +import 'transfer_option_widget.dart'; +import 'update_option_widget.dart'; class NameDetailsView extends ConsumerStatefulWidget { const NameDetailsView({ @@ -232,6 +235,122 @@ class _ManageDomainsWidgetState extends ConsumerState { child: child, ), ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Transfer", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Transfer domain", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: TransferOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + const SizedBox( + width: 32, + ), + Expanded( + child: SecondaryButton( + label: "Update", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Update domain", + style: STextStyles.desktopH3( + context, + ), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: UpdateOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), ], ), ); @@ -285,60 +404,6 @@ class _ManageDomainsWidgetState extends ConsumerState { ), ], ), - if (Util.isDesktop && utxo != null) - CustomTextButton( - text: "Manage", - onTap: () { - showDialog( - context: context, - builder: (context) { - return SDialog( - child: SizedBox( - width: 641, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.only( - left: 32, - ), - child: Text( - "Manage domain", - style: - STextStyles.desktopH3( - context, - ), - ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - ), - child: IntrinsicHeight( - child: ManageDomainView( - walletId: widget.walletId, - utxo: utxo!, - ), - ), - ), - ], - ), - ), - ); - }, - ); - }, - ), if (!Util.isDesktop) SelectableText( opNameData!.op.name, diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 1bd9c9cde..3ac435b8c 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -19,7 +19,10 @@ import '../../../wallets/isar/providers/wallet_info_provider.dart'; import '../../../wallets/models/name_op_state.dart'; import '../../../wallets/models/tx_data.dart'; import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/conditional_parent.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; import '../../../widgets/desktop/primary_button.dart'; +import '../../../widgets/desktop/secondary_button.dart'; import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/icon_widgets/addressbook_icon.dart'; import '../../../widgets/icon_widgets/clipboard_icon.dart'; @@ -78,31 +81,62 @@ class _TransferOptionWidgetState extends ConsumerState { bool wasCancelled = false; if (mounted) { - unawaited( - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return BuildingTransactionDialog( - coin: wallet.info.coin, - isSpark: false, - onCancel: () { - wasCancelled = true; - Navigator.of(context).pop(); - }, - ); - }, - ), - ); + if (Util.isDesktop) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ); + }, + ), + ); + } else { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } } + final opName = wallet.getOpNameDataFrom(widget.utxo)!; + final time = Future.delayed( const Duration( milliseconds: 2500, ), ); + final nameScriptHex = scriptNameUpdate(opName.fullname, opName.value); + final txDataFuture = wallet.prepareNameSend( txData: TxData( feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? @@ -116,23 +150,17 @@ class _TransferOptionWidgetState extends ConsumerState { ), ), ], - note: "Transfer domain name", + note: "Transfer ${opName.constructedName}", opNameState: NameOpState( - name: "", + name: opName.fullname, saltHex: "", commitment: "", - value: "", - nameScriptHex: "", + value: opName.value, + nameScriptHex: nameScriptHex, type: OpName.nameUpdate, output: widget.utxo, outputPosition: -1, //currently unknown, updated later ), - // satsPerVByte: isCustomFee ? customFeeRate : null, - // utxos: (wallet is CoinControlInterface && - // coinControlEnabled && - // selectedUTXOs.isNotEmpty) - // ? selectedUTXOs - // : null, ), ); @@ -405,19 +433,43 @@ class _TransferOptionWidgetState extends ConsumerState { ), ), SizedBox( - height: Util.isDesktop ? 24 : 16, - ), - // if (!Util.isDesktop) const Spacer(), - PrimaryButton( - label: "Transfer", - enabled: _enableButton, - // width: Util.isDesktop ? 160 : double.infinity, - buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: _preview, + height: Util.isDesktop ? 42 : 16, ), - SizedBox( - height: Util.isDesktop ? 24 : 16, + if (!Util.isDesktop) const Spacer(), + ConditionalParent( + condition: Util.isDesktop, + builder: (child) => Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: ButtonHeight.l, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: child, + ), + ], + ), + child: PrimaryButton( + label: "Transfer", + enabled: _enableButton, + // width: Util.isDesktop ? 160 : double.infinity, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _preview, + ), ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), ], ); } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 10c804022..94bcae938 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -595,6 +595,7 @@ class NamecoinWallet value: data.value, nameScriptHex: nameScriptHex, type: OpName.nameFirstUpdate, + output: utxo, outputPosition: -1, //currently unknown, updated later ), note: "Purchase $noteName", @@ -989,7 +990,8 @@ class NamecoinWallet throw Exception("Coin control used where utxos is null!"); } - if (txData.opNameState!.type == OpName.nameUpdate && + if ((txData.opNameState!.type == OpName.nameFirstUpdate || + txData.opNameState!.type == OpName.nameUpdate) && txData.opNameState!.output == null) { throw Exception("Missing name output to update"); } @@ -1018,7 +1020,8 @@ class NamecoinWallet final List availableOutputs = utxos ?? await mainDB.getUTXOs(walletId).findAll(); - if (txData.opNameState!.type == OpName.nameUpdate) { + if (txData.opNameState!.type == OpName.nameUpdate || + txData.opNameState!.type == OpName.nameFirstUpdate) { // name output is added later availableOutputs.removeWhere((e) => e == txData.opNameState!.output!); } @@ -1027,111 +1030,19 @@ class NamecoinWallet final canCPFP = this is CpfpInterface && coinControl; - int nameOutputCount = 0; // for sanity check. Should only be max 1; - void nameOutputCountCheck() { - nameOutputCount++; - if (nameOutputCount > 1) { - throw Exception("nameOutputCount greater than one"); - } - } - - final List spendableOutputs; - switch (txData.opNameState!.type) { - case OpName.nameNew: - spendableOutputs = availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); - break; - - case OpName.nameFirstUpdate: - spendableOutputs = availableOutputs.where( - (e) { - if (e.used == true) return false; - - final nameOp = getOpNameDataFrom(e); - if (nameOp != null) { - if (nameOp.op == OpName.nameFirstUpdate || - nameOp.op == OpName.nameUpdate) { - return false; - } else { - final confirmed = e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - overrideMinConfirms: kNameWaitBlocks, - ); - - if (confirmed) { - nameOutputCountCheck(); - } - return confirmed; - } - } else { - return canCPFP || + final spendableOutputs = availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || e.isConfirmed( currentChainHeight, cryptoCurrency.minConfirms, cryptoCurrency.minCoinbaseConfirms, - ); - } - }, - ).toList(); - break; - - case OpName.nameUpdate: - spendableOutputs = availableOutputs.where( - (e) { - if (e.used == true) return false; - - final nameOp = getOpNameDataFrom(e); - if (nameOp != null) { - if (nameOp.op == OpName.nameFirstUpdate || - nameOp.op == OpName.nameUpdate) { - final confirmed = e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - overrideMinConfirms: kNameWaitBlocks, - ); - - if (confirmed) { - nameOutputCountCheck(); - } - return confirmed; - } else { - return false; - } - } else { - return canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - ); - } - }, - ).toList(); - break; - } - - final spendableSatoshiValue = - spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); - - if (spendableSatoshiValue < satoshiAmountToSend) { - throw Exception("Insufficient balance"); - } else if (spendableSatoshiValue == satoshiAmountToSend) { - throw Exception("Insufficient balance to pay transaction fee"); - } + )), + ) + .toList(); if (coinControl) { if (spendableOutputs.length < availableOutputs.length) { @@ -1146,6 +1057,20 @@ class NamecoinWallet ); } + // add name output to modify + if (txData.opNameState!.type == OpName.nameUpdate || + txData.opNameState!.type == OpName.nameFirstUpdate) { + spendableOutputs.insert(0, txData.opNameState!.output!); + } + + final spendableSatoshiValue = + spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); + + if (spendableSatoshiValue < satoshiAmountToSend) { + throw Exception("Insufficient balance"); + } else if (spendableSatoshiValue == satoshiAmountToSend) { + throw Exception("Insufficient balance to pay transaction fee"); + } Logging.instance.d( "spendableOutputs.length: ${spendableOutputs.length}" "\navailableOutputs.length: ${availableOutputs.length}" @@ -1156,10 +1081,7 @@ class NamecoinWallet BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; - final List utxoObjectsToUse = [ - if (txData.opNameState!.type == OpName.nameUpdate) - txData.opNameState!.output!, - ]; + final List utxoObjectsToUse = []; if (!coinControl) { for (int i = 0; From 9dbf80d2b662a82a343950fdfdd89102fe787dcc Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Feb 2025 15:39:24 -0600 Subject: [PATCH 28/29] disable manage for pre reg names for now --- .../sub_widgets/name_details.dart | 220 +++++++++--------- 1 file changed, 113 insertions(+), 107 deletions(-) diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index fbcbb01f3..0e42a0a2e 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -143,6 +143,10 @@ class _ManageDomainsWidgetState extends ConsumerState { final isExpired = opNameData?.expired(currentHeight) == true; final isSemiExpired = opNameData?.expired(currentHeight, true) == true; + final canManage = utxo != null && + (opNameData?.op == OpName.nameUpdate || + opNameData?.op == OpName.nameFirstUpdate); + return ConditionalParent( condition: !Util.isDesktop, builder: (child) => Background( @@ -156,9 +160,8 @@ class _ManageDomainsWidgetState extends ConsumerState { "Domain details", style: STextStyles.navBarTitle(context), ), - actions: utxo == null - ? null - : [ + actions: canManage + ? [ Padding( padding: const EdgeInsets.only( top: 10, @@ -176,7 +179,8 @@ class _ManageDomainsWidgetState extends ConsumerState { }, ), ), - ], + ] + : null, ), body: SafeArea( child: LayoutBuilder( @@ -235,122 +239,124 @@ class _ManageDomainsWidgetState extends ConsumerState { child: child, ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Transfer", - buttonHeight: ButtonHeight.l, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return SDialog( - child: SizedBox( - width: 641, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Transfer domain", - style: STextStyles.desktopH3( - context, + if (canManage) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Transfer", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Transfer domain", + style: STextStyles.desktopH3( + context, + ), ), ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, - top: 16, + const DesktopDialogCloseButton(), + ], ), - child: TransferOptionWidget( - walletId: widget.walletId, - utxo: utxo!, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: TransferOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), ), - ), - ], + ], + ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), - ), - const SizedBox( - width: 32, - ), - Expanded( - child: SecondaryButton( - label: "Update", - buttonHeight: ButtonHeight.l, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return SDialog( - child: SizedBox( - width: 641, - child: Column( - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: Text( - "Update domain", - style: STextStyles.desktopH3( - context, + const SizedBox( + width: 32, + ), + Expanded( + child: SecondaryButton( + label: "Update", + buttonHeight: ButtonHeight.l, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return SDialog( + child: SizedBox( + width: 641, + child: Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Update domain", + style: STextStyles.desktopH3( + context, + ), ), ), - ), - const DesktopDialogCloseButton(), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - bottom: 32, + const DesktopDialogCloseButton(), + ], ), - child: UpdateOptionWidget( - walletId: widget.walletId, - utxo: utxo!, + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: UpdateOptionWidget( + walletId: widget.walletId, + utxo: utxo!, + ), ), - ), - ], + ], + ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), - ), - ], + ], + ), + ), + if (canManage) + const SizedBox( + height: 32, ), - ), - const SizedBox( - height: 32, - ), ], ), ); From 0a90948b9c565a3979dd606e2f2b3043d2f5fc05 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Feb 2025 19:29:16 -0600 Subject: [PATCH 29/29] update name functionality w/ basic json pretty view --- .../sub_widgets/name_details.dart | 93 +++--- .../sub_widgets/owned_name_card.dart | 37 ++- .../sub_widgets/transfer_option_widget.dart | 6 +- .../sub_widgets/update_option_widget.dart | 307 ++++++++++++++++-- lib/utilities/text_formatters.dart | 38 ++- lib/wallets/wallet/impl/namecoin_wallet.dart | 4 +- 6 files changed, 389 insertions(+), 96 deletions(-) diff --git a/lib/pages/namecoin_names/sub_widgets/name_details.dart b/lib/pages/namecoin_names/sub_widgets/name_details.dart index 0e42a0a2e..7fa77f807 100644 --- a/lib/pages/namecoin_names/sub_widgets/name_details.dart +++ b/lib/pages/namecoin_names/sub_widgets/name_details.dart @@ -8,6 +8,7 @@ import 'package:namecoin/namecoin.dart'; import '../../../models/isar/models/isar_models.dart'; import '../../../providers/db/main_db_provider.dart'; import '../../../providers/global/secure_store_provider.dart'; +import '../../../providers/global/wallets_provider.dart'; import '../../../themes/stack_colors.dart'; import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; @@ -105,6 +106,47 @@ class _ManageDomainsWidgetState extends ConsumerState { } } + (String, Color) _getExpiry(int currentChainHeight, StackColors theme) { + final String message; + final Color color; + + if (utxo?.blockHash == null) { + message = "Expires in $blocksNameExpiration+ blocks"; + color = theme.accentColorGreen; + } else { + final remaining = opNameData?.expiredBlockLeft( + currentChainHeight, + false, + ); + final semiRemaining = opNameData?.expiredBlockLeft( + currentChainHeight, + true, + ); + + if (remaining == null) { + color = theme.accentColorRed; + message = "Expired"; + } else { + message = "Expires in $remaining blocks"; + if (semiRemaining == null) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } + } + } + + return (message, color); + } + + bool _checkConfirmedUtxo(int currentHeight) { + return (ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet) + .checkUtxoConfirmed( + utxo!, + currentHeight, + ); + } + @override void initState() { super.initState(); @@ -140,10 +182,13 @@ class _ManageDomainsWidgetState extends ConsumerState { Widget build(BuildContext context) { final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); - final isExpired = opNameData?.expired(currentHeight) == true; - final isSemiExpired = opNameData?.expired(currentHeight, true) == true; + final (message, color) = _getExpiry( + currentHeight, + Theme.of(context).extension()!, + ); final canManage = utxo != null && + _checkConfirmedUtxo(currentHeight) && (opNameData?.op == OpName.nameUpdate || opNameData?.op == OpName.nameFirstUpdate); @@ -616,45 +661,11 @@ class _ManageDomainsWidgetState extends ConsumerState { const SizedBox( height: 4, ), - Row( - children: [ - SelectableText( - isExpired - ? "Expired" - : "${opNameData!.expiredBlockLeft(currentHeight)!}", - style: STextStyles.w500_14(context).copyWith( - color: isExpired - ? Theme.of(context) - .extension()! - .accentColorRed - : isSemiExpired - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - ), - ), - if (!isExpired) - Text( - " blocks remaining", - style: - STextStyles.w500_14(context).copyWith( - color: isExpired - ? Theme.of(context) - .extension()! - .accentColorRed - : isSemiExpired - ? Theme.of(context) - .extension()! - .accentColorYellow - : Theme.of(context) - .extension()! - .accentColorGreen, - ), - ), - ], + SelectableText( + message, + style: STextStyles.w500_14(context).copyWith( + color: color, + ), ), ], ), diff --git a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart index 74b6577c2..b345966c6 100644 --- a/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart +++ b/lib/pages/namecoin_names/sub_widgets/owned_name_card.dart @@ -43,24 +43,29 @@ class _OwnedNameCardState extends ConsumerState { final String message; final Color color; - final remaining = widget.opNameData.expiredBlockLeft( - currentChainHeight, - false, - ); - final semiRemaining = widget.opNameData.expiredBlockLeft( - currentChainHeight, - true, - ); - - if (remaining == null) { - color = theme.accentColorRed; - message = "Expired"; + if (widget.utxo.blockHash == null) { + message = "Expires in $blocksNameExpiration+ blocks"; + color = theme.accentColorGreen; } else { - message = "Expires in $remaining blocks"; - if (semiRemaining == null) { - color = theme.accentColorYellow; + final remaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + false, + ); + final semiRemaining = widget.opNameData.expiredBlockLeft( + currentChainHeight, + true, + ); + + if (remaining == null) { + color = theme.accentColorRed; + message = "Expired"; } else { - color = theme.accentColorGreen; + message = "Expires in $remaining blocks"; + if (semiRemaining == null) { + color = theme.accentColorYellow; + } else { + color = theme.accentColorGreen; + } } } diff --git a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart index 3ac435b8c..dcd8e1282 100644 --- a/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/transfer_option_widget.dart @@ -198,7 +198,11 @@ class _TransferOptionWidgetState extends ConsumerState { } } } catch (e, s) { - Logging.instance.e("_preview failed", error: e, stackTrace: s); + Logging.instance.e( + "_preview transfer name failed", + error: e, + stackTrace: s, + ); if (mounted) { String err = e.toString(); diff --git a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart index 64ffe7423..80c016245 100644 --- a/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart +++ b/lib/pages/namecoin_names/sub_widgets/update_option_widget.dart @@ -1,15 +1,31 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:namecoin/namecoin.dart'; import '../../../models/isar/models/blockchain_data/utxo.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../themes/stack_colors.dart'; +import '../../../utilities/amount/amount.dart'; import '../../../utilities/barcode_scanner_interface.dart'; import '../../../utilities/clipboard_interface.dart'; +import '../../../utilities/extensions/extensions.dart'; import '../../../utilities/logger.dart'; +import '../../../utilities/text_formatters.dart'; +import '../../../utilities/text_styles.dart'; import '../../../utilities/util.dart'; +import '../../../wallets/models/name_op_state.dart'; +import '../../../wallets/models/tx_data.dart'; +import '../../../wallets/wallet/impl/namecoin_wallet.dart'; +import '../../../widgets/desktop/desktop_dialog.dart'; +import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; +import '../../../widgets/dialogs/s_dialog.dart'; import '../../../widgets/stack_dialog.dart'; +import '../../send_view/sub_widgets/building_transaction_dialog.dart'; +import '../confirm_name_transaction_view.dart'; class UpdateOptionWidget extends ConsumerStatefulWidget { const UpdateOptionWidget({ @@ -31,15 +47,177 @@ class UpdateOptionWidget extends ConsumerStatefulWidget { } class _BuyDomainWidgetState extends ConsumerState { - final _nameController = TextEditingController(); - final _nameFieldFocus = FocusNode(); + final _controller = TextEditingController(); - bool _lookupLock = false; - Future _lookup() async { - if (_lookupLock) return; - _lookupLock = true; - try {} catch (e, s) { - Logging.instance.e("_lookup failed", error: e, stackTrace: s); + late final bool wasJson; + late final String _currentValue; + + String _getNewValue() { + final value = _controller.text; + try { + final json = jsonDecode(value); + final minified = jsonEncode(json); + return minified; + } catch (_) {} + return value; + } + + int _countLength() { + try { + final json = jsonDecode(_controller.text); + final minified = jsonEncode(json); + return minified.toUint8ListFromUtf8.lengthInBytes; + } catch (_) {} + + return _controller.text.toUint8ListFromUtf8.lengthInBytes; + } + + bool _previewLock = false; + Future _previewUpdate() async { + if (_previewLock) return; + _previewLock = true; + try { + final newValue = _getNewValue(); + if (newValue == _currentValue) { + throw Exception("Value was not changed!"); + } + + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + bool wasCancelled = false; + + if (mounted) { + if (Util.isDesktop) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + ); + }, + ), + ); + } else { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: wallet.info.coin, + isSpark: false, + onCancel: () { + wasCancelled = true; + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + } + + final _address = await wallet.getCurrentReceivingAddress(); + + final opName = wallet.getOpNameDataFrom(widget.utxo)!; + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + final nameScriptHex = scriptNameUpdate(opName.fullname, newValue); + + final txDataFuture = wallet.prepareNameSend( + txData: TxData( + feeRateType: kNameTxDefaultFeeRate, // TODO: make configurable? + recipients: [ + ( + address: _address!.value, + isChange: false, + amount: Amount( + rawValue: BigInt.from(kNameAmountSats), + fractionDigits: wallet.cryptoCurrency.fractionDigits, + ), + ), + ], + note: "Update ${opName.constructedName} (${opName.fullname})", + opNameState: NameOpState( + name: opName.fullname, + saltHex: "", + commitment: "", + value: newValue, + nameScriptHex: nameScriptHex, + type: OpName.nameUpdate, + output: widget.utxo, + outputPosition: -1, //currently unknown, updated later + ), + ), + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + final txData = results.first as TxData; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + + if (mounted) { + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => SDialog( + child: SizedBox( + width: 580, + child: ConfirmNameTransactionView( + txData: txData, + walletId: widget.walletId, + ), + ), + ), + ); + } else { + await Navigator.of(context).pushNamed( + ConfirmNameTransactionView.routeName, + arguments: (txData, widget.walletId), + ); + } + } + } + } catch (e, s) { + Logging.instance.e( + "_preview update name failed", + error: e, + stackTrace: s, + ); String? err; if (e.toString().contains("Contains invalid characters")) { @@ -50,7 +228,7 @@ class _BuyDomainWidgetState extends ConsumerState { await showDialog( context: context, builder: (_) => StackOkDialog( - title: "Name lookup failed", + title: "Update failed", message: err, desktopPopRootNavigator: Util.isDesktop, maxWidth: Util.isDesktop ? 600 : null, @@ -58,24 +236,32 @@ class _BuyDomainWidgetState extends ConsumerState { ); } } finally { - _lookupLock = false; + _previewLock = false; } } @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _nameFieldFocus.requestFocus(); - } - }); + final wallet = + ref.read(pWallets).getWallet(widget.walletId) as NamecoinWallet; + + _currentValue = wallet.getOpNameDataFrom(widget.utxo)!.value; + + // see if json, if so format nicely + try { + final json = jsonDecode(_currentValue); + _controller.text = const JsonEncoder.withIndent(" ").convert(json); + wasJson = true; + } catch (_) { + _controller.text = _currentValue; + wasJson = false; + } } @override void dispose() { - _nameController.dispose(); - _nameFieldFocus.dispose(); + _controller.dispose(); super.dispose(); } @@ -83,22 +269,87 @@ class _BuyDomainWidgetState extends ConsumerState { Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - Util.isDesktop ? CrossAxisAlignment.start : CrossAxisAlignment.center, + crossAxisAlignment: Util.isDesktop + ? CrossAxisAlignment.start + : CrossAxisAlignment.stretch, children: [ - SizedBox( - height: Util.isDesktop ? 24 : 16, + Text( + "Edit value", + style: STextStyles.label(context), + ), + const SizedBox( + height: 6, ), - SecondaryButton( - label: "Update", - enabled: _nameController.text.isNotEmpty, - // width: Util.isDesktop ? 160 : double.infinity, - buttonHeight: Util.isDesktop ? ButtonHeight.l : null, - onPressed: _lookup, + TextField( + controller: _controller, + maxLines: null, + autocorrect: false, + enableSuggestions: false, + style: const TextStyle(fontFamily: "monospace"), + onChanged: (_) { + setState(() {}); + }, + inputFormatters: [ + Utf8ByteLengthLimitingTextInputFormatter( + valueMaxLength, + tryMinifyJson: true, + ), + ], + ), + const SizedBox( + height: 4, + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Builder( + builder: (context) { + final length = _countLength(); + return Text( + "$length/$valueMaxLength", + style: STextStyles.w500_10(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ); + }, + ), + ], ), SizedBox( - height: Util.isDesktop ? 24 : 16, + height: Util.isDesktop ? 32 : 16, ), + if (!Util.isDesktop) const Spacer(), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: Navigator.of( + context, + rootNavigator: Util.isDesktop, + ).pop, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Update", + enabled: _controller.text.isNotEmpty, + buttonHeight: Util.isDesktop ? ButtonHeight.l : null, + onPressed: _previewUpdate, + ), + ), + ], + ), + if (!Util.isDesktop) + const SizedBox( + height: 16, + ), ], ); } diff --git a/lib/utilities/text_formatters.dart b/lib/utilities/text_formatters.dart index 70a96231d..a7cf76d4f 100644 --- a/lib/utilities/text_formatters.dart +++ b/lib/utilities/text_formatters.dart @@ -5,13 +5,31 @@ import 'package:flutter/services.dart'; class Utf8ByteLengthLimitingTextInputFormatter extends TextInputFormatter { Utf8ByteLengthLimitingTextInputFormatter( - this.maxBytes, - ) : assert(maxBytes == -1 || maxBytes > 0); + this.maxBytes, { + this.tryMinifyJson = false, + }) : assert(maxBytes == -1 || maxBytes > 0); final int maxBytes; + final bool tryMinifyJson; - static TextEditingValue truncate(TextEditingValue value, int maxBytes) { - final String text = value.text; + static String _maybeTryMinify(String text, bool tryMinifyJson) { + if (tryMinifyJson) { + try { + final json = jsonDecode(text); + final minified = jsonEncode(json); + return minified; + } catch (_) {} + } + + return text; + } + + static TextEditingValue truncate( + TextEditingValue value, + int maxBytes, + bool tryMinifyJson, + ) { + final String text = _maybeTryMinify(value.text, tryMinifyJson); final encoded = utf8.encode(text); if (encoded.length <= maxBytes) { @@ -47,17 +65,23 @@ class Utf8ByteLengthLimitingTextInputFormatter extends TextInputFormatter { TextEditingValue newValue, ) { if (maxBytes == -1 || - utf8.encode(newValue.text).lengthInBytes <= maxBytes) { + utf8 + .encode(_maybeTryMinify(newValue.text, tryMinifyJson)) + .lengthInBytes <= + maxBytes) { return newValue; } assert(maxBytes > 0); - if (utf8.encode(oldValue.text).lengthInBytes == maxBytes && + if (utf8 + .encode(_maybeTryMinify(oldValue.text, tryMinifyJson)) + .lengthInBytes == + maxBytes && oldValue.selection.isCollapsed) { return oldValue; } - return truncate(newValue, maxBytes); + return truncate(newValue, maxBytes, tryMinifyJson); } } diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 94bcae938..64dc23801 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -454,9 +454,7 @@ class NamecoinWallet } final data = decodeNameSaltData(encoded); - Logging.instance.e( - data, - ); + if (data.name == name) { return ( data: null,