diff --git a/packages/komodo_coins/lib/src/asset_filter.dart b/packages/komodo_coins/lib/src/asset_filter.dart index 0c6d0352..4e1a5fb4 100644 --- a/packages/komodo_coins/lib/src/asset_filter.dart +++ b/packages/komodo_coins/lib/src/asset_filter.dart @@ -31,7 +31,10 @@ class NoAssetFilterStrategy extends AssetFilterStrategy { /// ERC20, Arbitrum, and MATIC explicitly do not support Trezor via KDF /// at this time, so they are also excluded. class TrezorAssetFilterStrategy extends AssetFilterStrategy { - const TrezorAssetFilterStrategy() : super('trezor'); + const TrezorAssetFilterStrategy({this.hiddenAssets = const {}}) + : super('trezor'); + + final Set hiddenAssets; @override bool shouldInclude(Asset asset, JsonMap coinConfig) { @@ -39,9 +42,14 @@ class TrezorAssetFilterStrategy extends AssetFilterStrategy { // AVAX, BNB, ETH, FTM, etc. currently fail to activate on Trezor, // so we exclude them from the Trezor asset list. - return subClass == CoinSubClass.utxo || + final isProtocolSupported = subClass == CoinSubClass.utxo || subClass == CoinSubClass.smartChain || subClass == CoinSubClass.qrc20; + + final hasTrezorCoinField = coinConfig.containsKey('trezor_coin'); + final isExcludedAsset = hiddenAssets.contains(asset.id.id); + + return isProtocolSupported && hasTrezorCoinField && !isExcludedAsset; } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart b/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart index 951226d0..5b851868 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; /// Generic response details wrapper for task status responses -class ResponseDetails { +class ResponseDetails { ResponseDetails({required this.data, required this.error, this.description}) : assert( [data, error, description].where((e) => e != null).length == 1, @@ -14,7 +14,8 @@ class ResponseDetails { final R? error; // Usually only non-null for in-progress tasks - final String? description; + /// Additional status information for in-progress tasks + final D? description; void get throwIfError { if (error != null) { @@ -28,7 +29,9 @@ class ResponseDetails { return { if (data != null) 'data': jsonEncode(data), if (error != null) 'error': jsonEncode(error), - if (description != null) 'description': description, + if (description != null) + 'description': + description is String ? description : jsonEncode(description), }; } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart index 28556e88..e6728b2c 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart @@ -88,7 +88,11 @@ class AccountBalanceStatusResponse extends BaseResponse { mmrpc: json.value('mmrpc'), status: status!, // details: status == 'Ok' ? AccountBalanceInfo.fromJson(details) : details, - details: ResponseDetails( + details: ResponseDetails< + AccountBalanceInfo, + GeneralErrorResponse, + String + >( data: status == SyncStatusEnum.success ? AccountBalanceInfo.fromJson(result.value('details')) @@ -106,7 +110,8 @@ class AccountBalanceStatusResponse extends BaseResponse { } final SyncStatusEnum status; - final ResponseDetails details; + final ResponseDetails + details; @override JsonMap toJson() { diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart index ab2f8df0..45281ff6 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart @@ -95,36 +95,39 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { if (status == null) { throw FormatException( - 'Unrecognized task status: "$statusString". Expected one of: Ok, InProgress, Error', + 'Unrecognized task status: "$statusString". ' + 'Expected one of: Ok, InProgress, Error', ); } + final detailsJson = result['details']; + Object? description; + NewAddressInfo? data; + GeneralErrorResponse? error; + + if (status == SyncStatusEnum.success) { + data = NewAddressInfo.fromJson( + (detailsJson as JsonMap).value('new_address'), + ); + } else if (status == SyncStatusEnum.error) { + error = GeneralErrorResponse.parse(detailsJson as JsonMap); + } else if (status == SyncStatusEnum.inProgress) { + description = TaskDescriptionParserFactory.parseDescription(detailsJson); + } + return GetNewAddressTaskStatusResponse( mmrpc: json.value('mmrpc'), status: status, - details: ResponseDetails( - data: - status == SyncStatusEnum.success - ? NewAddressInfo.fromJson( - result - .value('details') - .value('new_address'), - ) - : null, - error: - status == SyncStatusEnum.error - ? GeneralErrorResponse.parse(result.value('details')) - : null, - description: - status == SyncStatusEnum.inProgress - ? result.value('details') - : null, + details: ResponseDetails( + data: data, + error: error, + description: description, ), ); } final SyncStatusEnum status; - final ResponseDetails details; + final ResponseDetails details; @override JsonMap toJson() { @@ -133,6 +136,41 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { 'result': {'status': status, 'details': details.toJson()}, }; } + + /// Convert this RPC response into a [NewAddressState]. + NewAddressState toNewAddressState(int taskId) { + switch (status) { + case SyncStatusEnum.success: + final addr = details.data!; + return NewAddressState( + status: NewAddressStatus.completed, + address: PubkeyInfo( + address: addr.address, + derivationPath: addr.derivationPath, + chain: addr.chain, + balance: addr.balance, + ), + taskId: taskId, + ); + case SyncStatusEnum.error: + return NewAddressState( + status: NewAddressStatus.error, + error: details.error?.error ?? 'Unknown error', + taskId: taskId, + ); + case SyncStatusEnum.inProgress: + return NewAddressState.fromInProgressDescription( + details.description, + taskId, + ); + case SyncStatusEnum.notStarted: + return NewAddressState( + status: NewAddressStatus.error, + error: 'Task not started', + taskId: taskId, + ); + } + } } // Cancel Request diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/task_description_parser.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/task_description_parser.dart new file mode 100644 index 00000000..90dfac7c --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/task_description_parser.dart @@ -0,0 +1,39 @@ +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Task Description Parser Strategy Pattern +abstract class TaskDescriptionParser { + bool canParse(JsonMap json); + Object parse(JsonMap json); +} + +class ConfirmAddressDescriptionParser implements TaskDescriptionParser { + @override + bool canParse(JsonMap json) => json.containsKey('ConfirmAddress'); + + @override + Object parse(JsonMap json) => + ConfirmAddressDetails.fromJson(json.value('ConfirmAddress')); +} + +class TaskDescriptionParserFactory { + static final List _parsers = [ + ConfirmAddressDescriptionParser(), + ]; + + static Object? parseDescription(Object? detailsJson) { + if (detailsJson is String) { + return detailsJson; + } else if (detailsJson is JsonMap) { + for (final parser in _parsers) { + if (parser.canParse(detailsJson)) { + return parser.parse(detailsJson); + } + } + + // Fallback to raw JsonMap + return detailsJson; + } + return null; + } +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart index 51437110..f5ed1737 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart @@ -24,6 +24,7 @@ export 'hd_wallet/get_new_address_task.dart'; export 'hd_wallet/hd_wallet_methods.dart'; export 'hd_wallet/scan_for_new_addresses_init.dart'; export 'hd_wallet/scan_for_new_addresses_status.dart'; +export 'hd_wallet/task_description_parser.dart'; export 'methods.dart'; export 'nft/enable_nft.dart'; export 'nft/nft_rpc_namespace.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart index e7f8dd30..be9dfd05 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart @@ -110,6 +110,20 @@ class ContextPrivKeyHDWalletStrategy extends PubkeyStrategy with HDWalletMixin { balance: newAddress.balance, ); } + + @override + Stream getNewAddressStream( + AssetId assetId, + ApiClient client, + ) async* { + try { + yield const NewAddressState(status: NewAddressStatus.processing); + final info = await getNewAddress(assetId, client); + yield NewAddressState.completed(info); + } catch (e) { + yield NewAddressState.error('Failed to generate address: $e'); + } + } } /// HD wallet strategy for Trezor wallets @@ -131,10 +145,48 @@ class TrezorHDWalletStrategy extends PubkeyStrategy with HDWalletMixin { ); } + @override + Stream getNewAddressStream( + AssetId assetId, + ApiClient client, { + Duration pollingInterval = const Duration(milliseconds: 200), + }) async* { + try { + final initResponse = await client.rpc.hdWallet.getNewAddressTaskInit( + coin: assetId.id, + accountId: 0, + chain: 'External', + gapLimit: _gapLimit, + ); + + var finished = false; + while (!finished) { + final status = await client.rpc.hdWallet.getNewAddressTaskStatus( + taskId: initResponse.taskId, + forgetIfFinished: false, + ); + + final state = status.toNewAddressState(initResponse.taskId); + yield state; + + if (state.status == NewAddressStatus.completed || + state.status == NewAddressStatus.error || + state.status == NewAddressStatus.cancelled) { + finished = true; + } else { + await Future.delayed(pollingInterval); + } + } + } catch (e) { + yield NewAddressState.error('Failed to generate address: $e'); + } + } + Future _getNewAddressTask( AssetId assetId, - ApiClient client, - ) async { + ApiClient client, { + Duration pollingInterval = const Duration(milliseconds: 200), + }) async { final initResponse = await client.rpc.hdWallet.getNewAddressTaskInit( coin: assetId.id, accountId: 0, @@ -150,7 +202,7 @@ class TrezorHDWalletStrategy extends PubkeyStrategy with HDWalletMixin { ); result = (status.details..throwIfError).data; - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(pollingInterval); } return result; } diff --git a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/single_address_strategy.dart b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/single_address_strategy.dart index 3dbf3f25..8a1ac790 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/single_address_strategy.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/single_address_strategy.dart @@ -41,6 +41,16 @@ class SingleAddressStrategy extends PubkeyStrategy { ); } + @override + Stream getNewAddressStream( + AssetId assetId, + ApiClient client, + ) async* { + yield NewAddressState.error( + 'Single address coins do not support generating new addresses', + ); + } + @override Future scanForNewAddresses(AssetId _, ApiClient __) async { // No-op for single address coins diff --git a/packages/komodo_defi_sdk/example/lib/screens/asset_page.dart b/packages/komodo_defi_sdk/example/lib/screens/asset_page.dart index f79ce769..f3567d17 100644 --- a/packages/komodo_defi_sdk/example/lib/screens/asset_page.dart +++ b/packages/komodo_defi_sdk/example/lib/screens/asset_page.dart @@ -47,10 +47,19 @@ class _AssetPageState extends State { Future _generateNewAddress() async { setState(() => _isLoading = true); try { - final newPubkey = await _sdk.pubkeys.createNewPubkey(widget.asset); - setState(() { - _pubkeys?.keys.add(newPubkey); - }); + final stream = _sdk.pubkeys.createNewPubkeyStream(widget.asset); + + final newPubkey = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _NewAddressDialog(stream: stream), + ); + + if (newPubkey != null) { + setState(() { + _pubkeys?.keys.add(newPubkey); + }); + } } catch (e) { setState(() => _error = e.toString()); } finally { @@ -150,7 +159,7 @@ class _AssetHeaderState extends State { _balance = balance; }); }, - onError: (error) { + onError: (Object error) { setState(() { _balanceLoading = false; _balanceError = error.toString(); @@ -260,7 +269,7 @@ class _AssetHeaderState extends State { _balance = balance; }); }, - onError: (error) { + onError: (Object error) { setState(() { _balanceLoading = false; _balanceError = error.toString(); @@ -655,3 +664,114 @@ class __TransactionsSectionState extends State<_TransactionsSection> { } } } + +class _NewAddressDialog extends StatefulWidget { + const _NewAddressDialog({required this.stream}); + + final Stream stream; + + @override + State<_NewAddressDialog> createState() => _NewAddressDialogState(); +} + +class _NewAddressDialogState extends State<_NewAddressDialog> { + late final StreamSubscription _subscription; + NewAddressState? _state; + + @override + void initState() { + super.initState(); + _subscription = widget.stream.listen((state) { + setState(() => _state = state); + if (state.status == NewAddressStatus.completed) { + Navigator.of(context).pop(state.address); + } else if (state.status == NewAddressStatus.cancelled) { + Navigator.of(context).pop(null); + } + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + Future _cancelAddressGeneration() async { + final state = _state; + if (state?.taskId != null) { + try { + final sdk = context.read(); + await sdk.client.rpc.hdWallet.getNewAddressTaskCancel( + taskId: state!.taskId!, + ); + } catch (e) { + // If cancellation fails, still dismiss the dialog + // The error is likely due to the task already being completed or cancelled + } + } + + // Always dismiss the dialog + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + final state = _state; + + String message; + if (state == null) { + message = 'Initializing...'; + } else { + switch (state.status) { + case NewAddressStatus.initializing: + case NewAddressStatus.processing: + case NewAddressStatus.waitingForDevice: + case NewAddressStatus.waitingForDeviceConfirmation: + case NewAddressStatus.pinRequired: + case NewAddressStatus.passphraseRequired: + message = state.message ?? 'Processing...'; + break; + case NewAddressStatus.confirmAddress: + message = 'Confirm the address on your device'; + break; + case NewAddressStatus.completed: + message = 'Completed'; + break; + case NewAddressStatus.error: + message = state.error ?? 'Error'; + break; + case NewAddressStatus.cancelled: + message = 'Cancelled'; + break; + } + } + + final showAddress = state?.status == NewAddressStatus.confirmAddress; + + return AlertDialog( + title: const Text('Generating Address'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (showAddress) + SelectableText(state?.expectedAddress ?? '') + else + const SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator(), + ), + const SizedBox(height: 16), + Text(message, textAlign: TextAlign.center), + ], + ), + actions: [ + TextButton( + onPressed: _cancelAddressGeneration, + child: const Text('Cancel'), + ), + ], + ); + } +} diff --git a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart index b450f539..5f2d077b 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart @@ -141,7 +141,7 @@ class AssetManager implements IAssetProvider { // WalletConnect and Metamask will require similar handling in the future. final strategy = isTrezor - ? const TrezorAssetFilterStrategy() + ? const TrezorAssetFilterStrategy(hiddenAssets: {'BCH'}) : const NoAssetFilterStrategy(); setFilterStrategy(strategy); diff --git a/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart b/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart index ec7a7217..fa998403 100644 --- a/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart @@ -30,6 +30,19 @@ class PubkeyManager { return strategy.getNewAddress(asset.id, _client); } + /// Streamed version of [createNewPubkey] + Stream createNewPubkeyStream(Asset asset) async* { + await retry(() => _activationManager.activateAsset(asset).last); + final strategy = await _resolvePubkeyStrategy(asset); + if (!strategy.supportsMultipleAddresses) { + yield NewAddressState.error( + 'Asset ${asset.id.name} does not support multiple addresses', + ); + return; + } + yield* strategy.getNewAddressStream(asset.id, _client); + } + Future _resolvePubkeyStrategy(Asset asset) async { final currentUser = await _auth.currentUser; if (currentUser == null) { diff --git a/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.dart b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.dart new file mode 100644 index 00000000..d243e3fa --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.dart @@ -0,0 +1,16 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +part 'confirm_address_details.freezed.dart'; +part 'confirm_address_details.g.dart'; + +/// Details returned when the hardware wallet asks to confirm an address. +@freezed +abstract class ConfirmAddressDetails with _$ConfirmAddressDetails { + const factory ConfirmAddressDetails({ + @JsonKey(name: 'expected_address') required String expectedAddress, + }) = _ConfirmAddressDetails; + + factory ConfirmAddressDetails.fromJson(JsonMap json) => + _$ConfirmAddressDetailsFromJson(json); +} diff --git a/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.freezed.dart b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.freezed.dart new file mode 100644 index 00000000..a5ae5b3f --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.freezed.dart @@ -0,0 +1,166 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'confirm_address_details.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ConfirmAddressDetails { + @JsonKey(name: 'expected_address') + String get expectedAddress; + + /// Create a copy of ConfirmAddressDetails + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $ConfirmAddressDetailsCopyWith get copyWith => + _$ConfirmAddressDetailsCopyWithImpl( + this as ConfirmAddressDetails, _$identity); + + /// Serializes this ConfirmAddressDetails to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is ConfirmAddressDetails && + (identical(other.expectedAddress, expectedAddress) || + other.expectedAddress == expectedAddress)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, expectedAddress); + + @override + String toString() { + return 'ConfirmAddressDetails(expectedAddress: $expectedAddress)'; + } +} + +/// @nodoc +abstract mixin class $ConfirmAddressDetailsCopyWith<$Res> { + factory $ConfirmAddressDetailsCopyWith(ConfirmAddressDetails value, + $Res Function(ConfirmAddressDetails) _then) = + _$ConfirmAddressDetailsCopyWithImpl; + @useResult + $Res call({@JsonKey(name: 'expected_address') String expectedAddress}); +} + +/// @nodoc +class _$ConfirmAddressDetailsCopyWithImpl<$Res> + implements $ConfirmAddressDetailsCopyWith<$Res> { + _$ConfirmAddressDetailsCopyWithImpl(this._self, this._then); + + final ConfirmAddressDetails _self; + final $Res Function(ConfirmAddressDetails) _then; + + /// Create a copy of ConfirmAddressDetails + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? expectedAddress = null, + }) { + return _then(_self.copyWith( + expectedAddress: null == expectedAddress + ? _self.expectedAddress + : expectedAddress // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _ConfirmAddressDetails implements ConfirmAddressDetails { + const _ConfirmAddressDetails( + {@JsonKey(name: 'expected_address') required this.expectedAddress}); + factory _ConfirmAddressDetails.fromJson(Map json) => + _$ConfirmAddressDetailsFromJson(json); + + @override + @JsonKey(name: 'expected_address') + final String expectedAddress; + + /// Create a copy of ConfirmAddressDetails + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$ConfirmAddressDetailsCopyWith<_ConfirmAddressDetails> get copyWith => + __$ConfirmAddressDetailsCopyWithImpl<_ConfirmAddressDetails>( + this, _$identity); + + @override + Map toJson() { + return _$ConfirmAddressDetailsToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _ConfirmAddressDetails && + (identical(other.expectedAddress, expectedAddress) || + other.expectedAddress == expectedAddress)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, expectedAddress); + + @override + String toString() { + return 'ConfirmAddressDetails(expectedAddress: $expectedAddress)'; + } +} + +/// @nodoc +abstract mixin class _$ConfirmAddressDetailsCopyWith<$Res> + implements $ConfirmAddressDetailsCopyWith<$Res> { + factory _$ConfirmAddressDetailsCopyWith(_ConfirmAddressDetails value, + $Res Function(_ConfirmAddressDetails) _then) = + __$ConfirmAddressDetailsCopyWithImpl; + @override + @useResult + $Res call({@JsonKey(name: 'expected_address') String expectedAddress}); +} + +/// @nodoc +class __$ConfirmAddressDetailsCopyWithImpl<$Res> + implements _$ConfirmAddressDetailsCopyWith<$Res> { + __$ConfirmAddressDetailsCopyWithImpl(this._self, this._then); + + final _ConfirmAddressDetails _self; + final $Res Function(_ConfirmAddressDetails) _then; + + /// Create a copy of ConfirmAddressDetails + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? expectedAddress = null, + }) { + return _then(_ConfirmAddressDetails( + expectedAddress: null == expectedAddress + ? _self.expectedAddress + : expectedAddress // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +// dart format on diff --git a/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.g.dart b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.g.dart new file mode 100644 index 00000000..7d21782c --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/confirm_address_details.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'confirm_address_details.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ConfirmAddressDetails _$ConfirmAddressDetailsFromJson( + Map json) => + _ConfirmAddressDetails( + expectedAddress: json['expected_address'] as String, + ); + +Map _$ConfirmAddressDetailsToJson( + _ConfirmAddressDetails instance) => + { + 'expected_address': instance.expectedAddress, + }; diff --git a/packages/komodo_defi_types/lib/src/public_key/new_address_state.dart b/packages/komodo_defi_types/lib/src/public_key/new_address_state.dart new file mode 100644 index 00000000..ccdb140f --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/new_address_state.dart @@ -0,0 +1,128 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart' + show ConfirmAddressDetails, PubkeyInfo; + +part 'new_address_state.freezed.dart'; +part 'new_address_state.g.dart'; + +@freezed +abstract class NewAddressState with _$NewAddressState { + const factory NewAddressState({ + required NewAddressStatus status, + String? message, + int? taskId, + NewAddressInfo? address, + String? expectedAddress, + String? error, + }) = _NewAddressState; + + const NewAddressState._(); + + /// Create a success state containing the generated address + + factory NewAddressState.completed(PubkeyInfo address) => + NewAddressState(status: NewAddressStatus.completed, address: address); + + factory NewAddressState.error(String error) => + NewAddressState(status: NewAddressStatus.error, error: error); + + /// Map in-progress descriptions to the appropriate state + factory NewAddressState.fromInProgressDescription( + Object? description, + int taskId, + ) { + if (description is ConfirmAddressDetails) { + return NewAddressState( + status: NewAddressStatus.confirmAddress, + expectedAddress: description.expectedAddress, + taskId: taskId, + ); + } + + final desc = description?.toString(); + + if (desc == null) { + return NewAddressState( + status: NewAddressStatus.initializing, + message: 'Generating new address...', + taskId: taskId, + ); + } + + final lower = desc.toLowerCase(); + + if (lower.contains('waiting') && lower.contains('connect')) { + return NewAddressState( + status: NewAddressStatus.waitingForDevice, + message: 'Waiting for device connection', + taskId: taskId, + ); + } + + if (lower.contains('follow') && lower.contains('instructions')) { + return NewAddressState( + status: NewAddressStatus.waitingForDeviceConfirmation, + message: 'Follow the instructions on your device', + taskId: taskId, + ); + } + + if (lower.contains('pin')) { + return NewAddressState( + status: NewAddressStatus.pinRequired, + message: 'Please enter your device PIN', + taskId: taskId, + ); + } + + if (lower.contains('passphrase')) { + return NewAddressState( + status: NewAddressStatus.passphraseRequired, + message: 'Please enter your device passphrase', + taskId: taskId, + ); + } + + return NewAddressState( + status: NewAddressStatus.processing, + message: desc, + taskId: taskId, + ); + } + + factory NewAddressState.fromJson(Map json) => + _$NewAddressStateFromJson(json); +} + +enum NewAddressStatus { + /// Generation process started + initializing, + + /// Waiting for the hardware wallet to be connected + waitingForDevice, + + /// Waiting for user confirmation on the device + waitingForDeviceConfirmation, + + /// The device requires a PIN entry + pinRequired, + + /// The device requires a passphrase entry + passphraseRequired, + + /// User must confirm the generated address on device + confirmAddress, + + /// Address generation is processing + processing, + + /// Address generation completed successfully + completed, + + /// An error occurred during generation + error, + + /// The operation was cancelled + cancelled, +} diff --git a/packages/komodo_defi_types/lib/src/public_key/new_address_state.freezed.dart b/packages/komodo_defi_types/lib/src/public_key/new_address_state.freezed.dart new file mode 100644 index 00000000..b8d2bfec --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/new_address_state.freezed.dart @@ -0,0 +1,258 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'new_address_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$NewAddressState { + NewAddressStatus get status; + String? get message; + int? get taskId; + NewAddressInfo? get address; + String? get expectedAddress; + String? get error; + + /// Create a copy of NewAddressState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $NewAddressStateCopyWith get copyWith => + _$NewAddressStateCopyWithImpl( + this as NewAddressState, _$identity); + + /// Serializes this NewAddressState to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is NewAddressState && + (identical(other.status, status) || other.status == status) && + (identical(other.message, message) || other.message == message) && + (identical(other.taskId, taskId) || other.taskId == taskId) && + (identical(other.address, address) || other.address == address) && + (identical(other.expectedAddress, expectedAddress) || + other.expectedAddress == expectedAddress) && + (identical(other.error, error) || other.error == error)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, status, message, taskId, address, expectedAddress, error); + + @override + String toString() { + return 'NewAddressState(status: $status, message: $message, taskId: $taskId, address: $address, expectedAddress: $expectedAddress, error: $error)'; + } +} + +/// @nodoc +abstract mixin class $NewAddressStateCopyWith<$Res> { + factory $NewAddressStateCopyWith( + NewAddressState value, $Res Function(NewAddressState) _then) = + _$NewAddressStateCopyWithImpl; + @useResult + $Res call( + {NewAddressStatus status, + String? message, + int? taskId, + NewAddressInfo? address, + String? expectedAddress, + String? error}); +} + +/// @nodoc +class _$NewAddressStateCopyWithImpl<$Res> + implements $NewAddressStateCopyWith<$Res> { + _$NewAddressStateCopyWithImpl(this._self, this._then); + + final NewAddressState _self; + final $Res Function(NewAddressState) _then; + + /// Create a copy of NewAddressState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? status = null, + Object? message = freezed, + Object? taskId = freezed, + Object? address = freezed, + Object? expectedAddress = freezed, + Object? error = freezed, + }) { + return _then(_self.copyWith( + status: null == status + ? _self.status + : status // ignore: cast_nullable_to_non_nullable + as NewAddressStatus, + message: freezed == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + taskId: freezed == taskId + ? _self.taskId + : taskId // ignore: cast_nullable_to_non_nullable + as int?, + address: freezed == address + ? _self.address + : address // ignore: cast_nullable_to_non_nullable + as NewAddressInfo?, + expectedAddress: freezed == expectedAddress + ? _self.expectedAddress + : expectedAddress // ignore: cast_nullable_to_non_nullable + as String?, + error: freezed == error + ? _self.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _NewAddressState extends NewAddressState { + const _NewAddressState( + {required this.status, + this.message, + this.taskId, + this.address, + this.expectedAddress, + this.error}) + : super._(); + factory _NewAddressState.fromJson(Map json) => + _$NewAddressStateFromJson(json); + + @override + final NewAddressStatus status; + @override + final String? message; + @override + final int? taskId; + @override + final NewAddressInfo? address; + @override + final String? expectedAddress; + @override + final String? error; + + /// Create a copy of NewAddressState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + _$NewAddressStateCopyWith<_NewAddressState> get copyWith => + __$NewAddressStateCopyWithImpl<_NewAddressState>(this, _$identity); + + @override + Map toJson() { + return _$NewAddressStateToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _NewAddressState && + (identical(other.status, status) || other.status == status) && + (identical(other.message, message) || other.message == message) && + (identical(other.taskId, taskId) || other.taskId == taskId) && + (identical(other.address, address) || other.address == address) && + (identical(other.expectedAddress, expectedAddress) || + other.expectedAddress == expectedAddress) && + (identical(other.error, error) || other.error == error)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, status, message, taskId, address, expectedAddress, error); + + @override + String toString() { + return 'NewAddressState(status: $status, message: $message, taskId: $taskId, address: $address, expectedAddress: $expectedAddress, error: $error)'; + } +} + +/// @nodoc +abstract mixin class _$NewAddressStateCopyWith<$Res> + implements $NewAddressStateCopyWith<$Res> { + factory _$NewAddressStateCopyWith( + _NewAddressState value, $Res Function(_NewAddressState) _then) = + __$NewAddressStateCopyWithImpl; + @override + @useResult + $Res call( + {NewAddressStatus status, + String? message, + int? taskId, + NewAddressInfo? address, + String? expectedAddress, + String? error}); +} + +/// @nodoc +class __$NewAddressStateCopyWithImpl<$Res> + implements _$NewAddressStateCopyWith<$Res> { + __$NewAddressStateCopyWithImpl(this._self, this._then); + + final _NewAddressState _self; + final $Res Function(_NewAddressState) _then; + + /// Create a copy of NewAddressState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? status = null, + Object? message = freezed, + Object? taskId = freezed, + Object? address = freezed, + Object? expectedAddress = freezed, + Object? error = freezed, + }) { + return _then(_NewAddressState( + status: null == status + ? _self.status + : status // ignore: cast_nullable_to_non_nullable + as NewAddressStatus, + message: freezed == message + ? _self.message + : message // ignore: cast_nullable_to_non_nullable + as String?, + taskId: freezed == taskId + ? _self.taskId + : taskId // ignore: cast_nullable_to_non_nullable + as int?, + address: freezed == address + ? _self.address + : address // ignore: cast_nullable_to_non_nullable + as NewAddressInfo?, + expectedAddress: freezed == expectedAddress + ? _self.expectedAddress + : expectedAddress // ignore: cast_nullable_to_non_nullable + as String?, + error: freezed == error + ? _self.error + : error // ignore: cast_nullable_to_non_nullable + as String?, + )); + } +} + +// dart format on diff --git a/packages/komodo_defi_types/lib/src/public_key/new_address_state.g.dart b/packages/komodo_defi_types/lib/src/public_key/new_address_state.g.dart new file mode 100644 index 00000000..6ba803db --- /dev/null +++ b/packages/komodo_defi_types/lib/src/public_key/new_address_state.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'new_address_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_NewAddressState _$NewAddressStateFromJson(Map json) => + _NewAddressState( + status: $enumDecode(_$NewAddressStatusEnumMap, json['status']), + message: json['message'] as String?, + taskId: (json['taskId'] as num?)?.toInt(), + address: json['address'] == null + ? null + : NewAddressInfo.fromJson(json['address'] as Map), + expectedAddress: json['expectedAddress'] as String?, + error: json['error'] as String?, + ); + +Map _$NewAddressStateToJson(_NewAddressState instance) => + { + 'status': _$NewAddressStatusEnumMap[instance.status]!, + 'message': instance.message, + 'taskId': instance.taskId, + 'address': instance.address, + 'expectedAddress': instance.expectedAddress, + 'error': instance.error, + }; + +const _$NewAddressStatusEnumMap = { + NewAddressStatus.initializing: 'initializing', + NewAddressStatus.waitingForDevice: 'waitingForDevice', + NewAddressStatus.waitingForDeviceConfirmation: 'waitingForDeviceConfirmation', + NewAddressStatus.pinRequired: 'pinRequired', + NewAddressStatus.passphraseRequired: 'passphraseRequired', + NewAddressStatus.confirmAddress: 'confirmAddress', + NewAddressStatus.processing: 'processing', + NewAddressStatus.completed: 'completed', + NewAddressStatus.error: 'error', + NewAddressStatus.cancelled: 'cancelled', +}; diff --git a/packages/komodo_defi_types/lib/src/public_key/pubkey_strategy.dart b/packages/komodo_defi_types/lib/src/public_key/pubkey_strategy.dart index 74d25b25..7c3c9980 100644 --- a/packages/komodo_defi_types/lib/src/public_key/pubkey_strategy.dart +++ b/packages/komodo_defi_types/lib/src/public_key/pubkey_strategy.dart @@ -12,6 +12,12 @@ abstract class PubkeyStrategy { /// Get a new address for an asset if supported Future getNewAddress(AssetId assetId, ApiClient client); + /// Streamed version of [getNewAddress] that emits progress updates + Stream getNewAddressStream( + AssetId assetId, + ApiClient client, + ); + /// Scan for any new addresses Future scanForNewAddresses(AssetId assetId, ApiClient client); diff --git a/packages/komodo_defi_types/lib/src/types.dart b/packages/komodo_defi_types/lib/src/types.dart index ed0ae9f7..b27f4ef0 100644 --- a/packages/komodo_defi_types/lib/src/types.dart +++ b/packages/komodo_defi_types/lib/src/types.dart @@ -40,7 +40,9 @@ export 'protocols/zhtlc/zhtlc_protocol.dart'; export 'public_key/address_operations.dart'; export 'public_key/asset_pubkeys.dart'; export 'public_key/balance_strategy.dart'; +export 'public_key/confirm_address_details.dart'; export 'public_key/derivation_method.dart'; +export 'public_key/new_address_state.dart'; export 'public_key/pubkey.dart'; export 'public_key/pubkey_strategy.dart'; export 'public_key/token_balance_map.dart'; diff --git a/packages/komodo_defi_types/lib/src/utils/backoff_strategy.dart b/packages/komodo_defi_types/lib/src/utils/backoff_strategy.dart index aad8849b..899660ab 100644 --- a/packages/komodo_defi_types/lib/src/utils/backoff_strategy.dart +++ b/packages/komodo_defi_types/lib/src/utils/backoff_strategy.dart @@ -89,7 +89,7 @@ class ConstantBackoff implements BackoffStrategy { /// Creates a constant backoff strategy /// /// [delay] Fixed delay between retries (default: 1s) - ConstantBackoff({ + const ConstantBackoff({ this.delay = const Duration(seconds: 1), }); @@ -117,7 +117,7 @@ class LinearBackoff implements BackoffStrategy { /// [initialDelay] Starting delay between retries (default: 200ms) /// [increment] Amount to increase delay by after each attempt (default: 200ms) /// [maxDelay] Maximum delay between retries (default: 5s) - LinearBackoff({ + const LinearBackoff({ this.initialDelay = const Duration(milliseconds: 200), this.increment = const Duration(milliseconds: 200), this.maxDelay = const Duration(seconds: 5),