diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml index 99aeafeb..f4752358 100644 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -77,8 +77,8 @@ jobs: # run: dart pub global activate melos # - name: Bootstrap workspace # run: melos bootstrap - # - name: Run dry web build to generate assets (expected to fail) - # run: cd packages/komodo_defi_sdk/example && flutter build web --release || echo "Dry build completed (failure expected)" + - name: Run dry web build to generate assets (expected to fail) + run: cd packages/komodo_defi_sdk/example && flutter build web --release || echo "Dry build completed (failure expected)" - name: Build SDK example web run: cd packages/komodo_defi_sdk/example && flutter build web --release - uses: FirebaseExtended/action-hosting-deploy@v0 diff --git a/packages/komodo_coins/lib/komodo_coins.dart b/packages/komodo_coins/lib/komodo_coins.dart index cfbe2cb8..eb943c63 100644 --- a/packages/komodo_coins/lib/komodo_coins.dart +++ b/packages/komodo_coins/lib/komodo_coins.dart @@ -1,6 +1,7 @@ /// TODO! Library description library komodo_coins; +export 'src/asset_filter.dart'; export 'src/komodo_coins_base.dart'; /// A Calculator. diff --git a/packages/komodo_coins/lib/src/asset_filter.dart b/packages/komodo_coins/lib/src/asset_filter.dart new file mode 100644 index 00000000..0c6d0352 --- /dev/null +++ b/packages/komodo_coins/lib/src/asset_filter.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Strategy interface for filtering assets based on coin configuration. +abstract class AssetFilterStrategy extends Equatable { + const AssetFilterStrategy(this.strategyId); + + /// A unique id for the strategy used for comparison and caching. + final String strategyId; + + /// Returns `true` if the asset should be included. + bool shouldInclude(Asset asset, JsonMap coinConfig); + + @override + List get props => [strategyId]; +} + +/// Default strategy that includes all assets. +class NoAssetFilterStrategy extends AssetFilterStrategy { + const NoAssetFilterStrategy() : super('none'); + + @override + bool shouldInclude(Asset asset, JsonMap coinConfig) => true; +} + +/// Filters assets that are not currently supported on Trezor. +/// This includes assets that are not UTXO-based or EVM-based tokens. +/// ETH, AVAX, BNB, FTM, etc. are excluded as they currently fail to +/// activate on Trezor. +/// 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'); + + @override + bool shouldInclude(Asset asset, JsonMap coinConfig) { + final subClass = asset.protocol.subClass; + + // AVAX, BNB, ETH, FTM, etc. currently fail to activate on Trezor, + // so we exclude them from the Trezor asset list. + return subClass == CoinSubClass.utxo || + subClass == CoinSubClass.smartChain || + subClass == CoinSubClass.qrc20; + } +} + +/// Filters out assets that are not UTXO-based chains. +class UtxoAssetFilterStrategy extends AssetFilterStrategy { + const UtxoAssetFilterStrategy() : super('utxo'); + + @override + bool shouldInclude(Asset asset, JsonMap coinConfig) { + final subClass = asset.protocol.subClass; + return subClass == CoinSubClass.utxo || subClass == CoinSubClass.smartChain; + } +} + +/// Filters assets that are EVM-based tokens. +/// This includes various EVM-compatible chains like Ethereum, Binance, etc. +/// This strategy is necessary for external wallets like Metamask or +/// WalletConnect. +class EvmAssetFilterStrategy extends AssetFilterStrategy { + const EvmAssetFilterStrategy() : super('evm'); + + @override + bool shouldInclude(Asset asset, JsonMap coinConfig) => + evmCoinSubClasses.contains(asset.protocol.subClass); +} diff --git a/packages/komodo_coins/lib/src/komodo_coins_base.dart b/packages/komodo_coins/lib/src/komodo_coins_base.dart index 2acf5e5d..28385e01 100644 --- a/packages/komodo_coins/lib/src/komodo_coins_base.dart +++ b/packages/komodo_coins/lib/src/komodo_coins_base.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:komodo_coins/src/config_transform.dart'; +import 'package:komodo_coins/src/asset_filter.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -17,6 +18,7 @@ class KomodoCoins { } Map? _assets; + final Map> _filterCache = {}; @mustCallSuper Future init() async { @@ -76,8 +78,10 @@ class KomodoCoins { try { // Parse all possible AssetIds for this coin - final assetIds = - AssetId.parseAllTypes(coinData, knownIds: platformIds).map( + final assetIds = AssetId.parseAllTypes( + coinData, + knownIds: platformIds, + ).map( (id) => id.isChildAsset ? AssetId.parse(coinData, knownIds: platformIds) : id, @@ -111,6 +115,31 @@ class KomodoCoins { coinData.valueOrNull('parent_coin') == null; } + /// Returns the assets filtered using the provided [strategy]. + /// + /// This allows higher-level components, such as [AssetManager], to tailor + /// the visible asset list to the active authentication context. For example, + /// a hardware wallet may only support a subset of coins, which can be + /// enforced by supplying an appropriate [AssetFilterStrategy]. + Map filteredAssets(AssetFilterStrategy strategy) { + if (!isInitialized) { + throw StateError('Assets have not been initialized. Call init() first.'); + } + final cacheKey = strategy.strategyId; + final cached = _filterCache[cacheKey]; + if (cached != null) return cached; + + final result = {}; + for (final entry in _assets!.entries) { + final config = entry.value.protocol.config; + if (strategy.shouldInclude(entry.value, config)) { + result[entry.key] = entry.value; + } + } + _filterCache[cacheKey] = result; + return result; + } + // Helper methods Asset? findByTicker(String ticker, CoinSubClass subClass) { return all.entries diff --git a/packages/komodo_coins/test/asset_filter_test.dart b/packages/komodo_coins/test/asset_filter_test.dart new file mode 100644 index 00000000..19cb28d2 --- /dev/null +++ b/packages/komodo_coins/test/asset_filter_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_coins/src/asset_filter.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +void main() { + group('Asset filtering', () { + final btcConfig = { + 'coin': 'BTC', + 'fname': 'Bitcoin', + 'chain_id': 0, + 'type': 'UTXO', + 'protocol': {'type': 'UTXO'}, + 'is_testnet': false, + 'trezor_coin': 'Bitcoin', + }; + + final ethConfig = { + 'coin': 'ETH', + 'fname': 'Ethereum', + 'chain_id': 1, + 'type': 'ERC-20', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 1}, + }, + 'nodes': [ + {'url': 'https://rpc'}, + ], + 'swap_contract_address': '0xabc', + 'fallback_swap_contract': '0xdef', + }; + + final btc = Asset.fromJson(btcConfig); + final eth = Asset.fromJson(ethConfig); + + test('Trezor filter excludes assets missing trezor_coin', () { + const filter = TrezorAssetFilterStrategy(); + expect(filter.shouldInclude(btc, btc.protocol.config), isTrue); + expect(filter.shouldInclude(eth, eth.protocol.config), isFalse); + + final assets = {btc.id: btc, eth.id: eth}; + final filtered = {}; + for (final entry in assets.entries) { + if (filter.shouldInclude(entry.value, entry.value.protocol.config)) { + filtered[entry.key] = entry.value; + } + } + + expect(filtered.containsKey(btc.id), isTrue); + expect(filtered.containsKey(eth.id), isFalse); + }); + + test('Trezor filter ignores empty trezor_coin field', () { + final cfg = Map.from(btcConfig)..['trezor_coin'] = ''; + final asset = Asset.fromJson(cfg); + const filter = TrezorAssetFilterStrategy(); + expect(filter.shouldInclude(asset, asset.protocol.config), isFalse); + }); + + test('UTXO filter only includes utxo assets', () { + const filter = UtxoAssetFilterStrategy(); + expect(filter.shouldInclude(btc, btc.protocol.config), isTrue); + expect(filter.shouldInclude(eth, eth.protocol.config), isFalse); + }); + + test('UTXO filter accepts smartChain subclass', () { + final cfg = Map.from(btcConfig) + ..['type'] = 'SMART_CHAIN' + ..['protocol'] = {'type': 'UTXO'}; + final asset = Asset.fromJson(cfg); + const filter = UtxoAssetFilterStrategy(); + expect(asset.protocol.subClass, CoinSubClass.smartChain); + expect(filter.shouldInclude(asset, asset.protocol.config), isTrue); + }); + }); +} diff --git a/packages/komodo_defi_framework/pubspec.yaml b/packages/komodo_defi_framework/pubspec.yaml index e7bfaa0f..aa748c5e 100644 --- a/packages/komodo_defi_framework/pubspec.yaml +++ b/packages/komodo_defi_framework/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: path: ../komodo_defi_types komodo_wallet_build_transformer: path: ../komodo_wallet_build_transformer - logging: ^1.2.0 + logging: ^1.3.0 mutex: ^3.1.0 path: any path_provider: ^2.1.4 diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart index 463a2e52..dc816e7a 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart @@ -4,6 +4,7 @@ import 'package:komodo_defi_local_auth/src/auth/auth_service.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:logging/logging.dart'; /// High level helper that handles sign in/register and Trezor device /// initialization for the built in "My Trezor" wallet. @@ -19,6 +20,7 @@ class TrezorAuthService implements IAuthService { static const String trezorWalletName = 'My Trezor'; static const String _passwordKey = 'trezor_wallet_password'; + static final _log = Logger('TrezorAuthService'); final IAuthService _authService; final TrezorRepository _trezor; @@ -38,33 +40,9 @@ class TrezorAuthService implements IAuthService { required AuthOptions options, }) async* { try { - // For Trezor, we need to use the built-in trezor wallet name - // and let TrezorAuthService handle the credentials - await for (final trezorState in _initializeTrezorAndAuthenticate( - derivationMethod: options.derivationMethod, - )) { - if (trezorState.status == AuthenticationStatus.completed) { - // TrezorAuthService already completed the sign-in process - // Just get the current user - final user = await _authService.getActiveUser(); - if (user != null) { - yield AuthenticationState.completed(user); - } else { - yield AuthenticationState.error( - 'Failed to retrieve signed-in user', - ); - } - break; - } - - yield trezorState.toAuthenticationState(); - - if (trezorState.status == AuthenticationStatus.error || - trezorState.status == AuthenticationStatus.cancelled) { - break; - } - } + yield* _authenticateTrezorStream(); } catch (e) { + await _signOutCurrentTrezorUser(); yield AuthenticationState.error('Trezor sign-in failed: $e'); } } @@ -75,34 +53,9 @@ class TrezorAuthService implements IAuthService { Mnemonic? mnemonic, }) async* { try { - // For Trezor, we need to use the built-in trezor wallet name - // and let TrezorAuthService handle the credentials - await for (final trezorState in _initializeTrezorAndAuthenticate( - derivationMethod: options.derivationMethod, - register: true, - )) { - yield trezorState.toAuthenticationState(); - - if (trezorState.status == AuthenticationStatus.completed) { - // TrezorAuthService already completed the registration process - // Just get the current user - final user = await _authService.getActiveUser(); - if (user != null) { - yield AuthenticationState.completed(user); - } else { - yield AuthenticationState.error( - 'Failed to retrieve registered user', - ); - } - break; - } - - if (trezorState.status == AuthenticationStatus.error || - trezorState.status == AuthenticationStatus.cancelled) { - break; - } - } + yield* _authenticateTrezorStream(); } catch (e) { + await _signOutCurrentTrezorUser(); yield AuthenticationState.error('Trezor registration failed: $e'); } } @@ -173,49 +126,10 @@ class TrezorAuthService implements IAuthService { } try { - // Copy over contents from the streamed function - await for (final trezorState in _initializeTrezorAndAuthenticate( - derivationMethod: options.derivationMethod, - )) { - // If status is passphrase required, use the provided password - if (trezorState.status == AuthenticationStatus.passphraseRequired) { - await _trezor.providePassphrase(trezorState.taskId!, password); - } - // Ignore pin required user action - user has to enter PIN on the device - - // Wait for task to finish and return result - if (trezorState.status == AuthenticationStatus.completed) { - final user = await _authService.getActiveUser(); - if (user != null) { - return user; - } else { - throw AuthException( - 'Failed to retrieve signed-in user', - type: AuthExceptionType.generalAuthError, - ); - } - } - - if (trezorState.status == AuthenticationStatus.error) { - throw AuthException( - trezorState.message ?? 'Trezor sign-in failed', - type: AuthExceptionType.generalAuthError, - ); - } - - if (trezorState.status == AuthenticationStatus.cancelled) { - throw AuthException( - 'Trezor sign-in was cancelled', - type: AuthExceptionType.generalAuthError, - ); - } - } - - throw AuthException( - 'Trezor sign-in did not complete', - type: AuthExceptionType.generalAuthError, - ); + return await _initializeTrezorWithPassphrase(passphrase: password); } catch (e) { + await _signOutCurrentTrezorUser(); + if (e is AuthException) rethrow; throw AuthException( 'Trezor sign-in failed: $e', @@ -240,50 +154,10 @@ class TrezorAuthService implements IAuthService { } try { - // Copy over contents from the streamed function - await for (final trezorState in _initializeTrezorAndAuthenticate( - derivationMethod: options.derivationMethod, - register: true, - )) { - // If status is passphrase required, use the provided password - if (trezorState.status == AuthenticationStatus.passphraseRequired) { - await _trezor.providePassphrase(trezorState.taskId!, password); - } - // Ignore pin required user action - user has to enter PIN on the device - - // Wait for task to finish and return result - if (trezorState.status == AuthenticationStatus.completed) { - final user = await _authService.getActiveUser(); - if (user != null) { - return user; - } else { - throw AuthException( - 'Failed to retrieve registered user', - type: AuthExceptionType.generalAuthError, - ); - } - } - - if (trezorState.status == AuthenticationStatus.error) { - throw AuthException( - trezorState.message ?? 'Trezor registration failed', - type: AuthExceptionType.generalAuthError, - ); - } - - if (trezorState.status == AuthenticationStatus.cancelled) { - throw AuthException( - 'Trezor registration was cancelled', - type: AuthExceptionType.generalAuthError, - ); - } - } - - throw AuthException( - 'Trezor registration did not complete', - type: AuthExceptionType.generalAuthError, - ); + return await _initializeTrezorWithPassphrase(passphrase: password); } catch (e) { + await _signOutCurrentTrezorUser(); + if (e is AuthException) rethrow; throw AuthException( 'Trezor registration failed: $e', @@ -319,6 +193,7 @@ class TrezorAuthService implements IAuthService { Future _signOutCurrentTrezorUser() async { final current = await _authService.getActiveUser(); if (current?.walletId.name == trezorWalletName) { + _log.warning("Signing out current '${current?.walletId.name}' user"); try { await _authService.signOut(); } catch (_) { @@ -333,23 +208,27 @@ class TrezorAuthService implements IAuthService { return users.firstWhereOrNull( (u) => u.walletId.name == trezorWalletName && - u.authOptions.privKeyPolicy == const PrivateKeyPolicy.trezor(), + u.walletId.authOptions.privKeyPolicy == + const PrivateKeyPolicy.trezor(), ); } /// Authenticates with the Trezor wallet (sign in or register) + /// [derivationMethod] The derivation method to use for the wallet. + /// Defaults to [DerivationMethod.hdWallet], since trezor requires HD wallet + /// RPCs to function. + /// [existingUser] The existing user to authenticate Future _authenticateWithTrezorWallet({ required KdfUser? existingUser, required String password, - required DerivationMethod derivationMethod, - required bool register, + DerivationMethod derivationMethod = DerivationMethod.hdWallet, }) async { final authOptions = AuthOptions( derivationMethod: derivationMethod, privKeyPolicy: const PrivateKeyPolicy.trezor(), ); - if (existingUser != null && !register) { + if (existingUser != null) { await _authService.signIn( walletName: trezorWalletName, password: password, @@ -379,23 +258,114 @@ class TrezorAuthService implements IAuthService { /// Registers or signs in to the "My Trezor" wallet and initializes the device /// /// Emits [TrezorInitializationState] updates while the device is initializing - Stream _initializeTrezorAndAuthenticate({ - required DerivationMethod derivationMethod, - bool register = false, - }) async* { + Stream _initializeTrezorAndAuthenticate( + DerivationMethod derivationMethod, + ) async* { await _signOutCurrentTrezorUser(); final existingUser = await _findExistingTrezorUser(); - final isNewUser = existingUser == null || register; + final isNewUser = existingUser == null; final password = await _getPassword(isNewUser: isNewUser); await _authenticateWithTrezorWallet( existingUser: existingUser, password: password, derivationMethod: derivationMethod, - register: register, ); yield* _initializeTrezorDevice(); } + + Stream _authenticateTrezorStream({ + DerivationMethod derivationMethod = DerivationMethod.hdWallet, + }) async* { + try { + await for (final trezorState in _initializeTrezorAndAuthenticate( + derivationMethod, + )) { + if (trezorState.status == AuthenticationStatus.completed) { + final user = await _authService.getActiveUser(); + if (user != null) { + yield AuthenticationState.completed(user); + } else { + yield AuthenticationState.error( + 'Failed to retrieve signed-in user', + ); + } + break; + } + + yield trezorState.toAuthenticationState(); + + if (trezorState.status == AuthenticationStatus.error || + trezorState.status == AuthenticationStatus.cancelled) { + await _signOutCurrentTrezorUser(); + break; + } + } + } catch (e) { + await _signOutCurrentTrezorUser(); + yield AuthenticationState.error('Trezor stream error: $e'); + } + } + + /// Initializes the Trezor device and handles passphrase input + /// This method is used for both sign-in and registration + /// It returns the authenticated [KdfUser] on success. + /// If the Trezor device requires a passphrase, it will provide the passphrase + /// and return the authenticated user. + /// If the Trezor device requires a PIN, it will ignore the PIN prompt and + /// wait for the user to enter the PIN on the device. + /// This method will throw an [AuthException] if the Trezor device + /// initialization fails or if the user is not authenticated successfully. + Future _initializeTrezorWithPassphrase({ + required String passphrase, + DerivationMethod derivationMethod = DerivationMethod.hdWallet, + }) async { + // Copy over contents from the streamed function + await for (final trezorState in _initializeTrezorAndAuthenticate( + derivationMethod, + )) { + // If status is passphrase required, use the provided password + if (trezorState.status == AuthenticationStatus.passphraseRequired) { + await _trezor.providePassphrase(trezorState.taskId!, passphrase); + } + // Ignore pin required user action - user has to enter PIN on the device + + // Wait for task to finish and return result + if (trezorState.status == AuthenticationStatus.completed) { + final user = await _authService.getActiveUser(); + if (user != null) { + return user; + } else { + throw AuthException( + 'Failed to retrieve registered user', + type: AuthExceptionType.generalAuthError, + ); + } + } + + if (trezorState.status == AuthenticationStatus.error) { + await _signOutCurrentTrezorUser(); + throw AuthException( + trezorState.message ?? 'Trezor registration failed', + type: AuthExceptionType.generalAuthError, + ); + } + + if (trezorState.status == AuthenticationStatus.cancelled) { + await _signOutCurrentTrezorUser(); + throw AuthException( + 'Trezor registration was cancelled', + type: AuthExceptionType.generalAuthError, + ); + } + } + + await _signOutCurrentTrezorUser(); + throw AuthException( + 'Trezor registration did not complete', + type: AuthExceptionType.generalAuthError, + ); + } } diff --git a/packages/komodo_defi_local_auth/pubspec.yaml b/packages/komodo_defi_local_auth/pubspec.yaml index 65fd5a59..0160a6b6 100644 --- a/packages/komodo_defi_local_auth/pubspec.yaml +++ b/packages/komodo_defi_local_auth/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: mutex: ^3.1.0 uuid: ^4.4.2 freezed_annotation: ^3.0.0 + logging: ^1.3.0 dev_dependencies: flutter_test: diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/tendermint_activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/tendermint_activation_params.dart index 61b52e7f..a8677aac 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/tendermint_activation_params.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/tendermint_activation_params.dart @@ -8,12 +8,15 @@ class TendermintActivationParams extends ActivationParams { required this.getBalances, required this.nodes, required this.txHistory, - super.requiredConfirmations = 3, - super.requiresNotarization = false, - super.privKeyPolicy = const PrivateKeyPolicy.contextPrivKey(), + super.requiredConfirmations, + super.requiresNotarization, + super.privKeyPolicy, }) : _tokensParams = tokensParams; - factory TendermintActivationParams.fromJson(JsonMap json) { + factory TendermintActivationParams.fromJson( + JsonMap json, { + PrivateKeyPolicy? privKeyPolicy, + }) { final base = ActivationParams.fromConfigJson(json); return TendermintActivationParams( @@ -32,9 +35,7 @@ class TendermintActivationParams extends ActivationParams { requiredConfirmations: base.requiredConfirmations, requiresNotarization: base.requiresNotarization, getBalances: json.valueOrNull('get_balances') ?? true, - privKeyPolicy: PrivateKeyPolicy.fromLegacyJson( - json.valueOrNull('priv_key_policy'), - ), + privKeyPolicy: privKeyPolicy ?? base.privKeyPolicy, nodes: json.value('rpc_urls').map(EvmNode.fromJson).toList(), ); } @@ -62,10 +63,10 @@ class TendermintActivationParams extends ActivationParams { tokensParams: tokensParams ?? _tokensParams, txHistory: txHistory ?? this.txHistory, requiredConfirmations: - requiredConfirmations ?? super.requiredConfirmations, - requiresNotarization: requiresNotarization ?? super.requiresNotarization, + requiredConfirmations ?? this.requiredConfirmations, + requiresNotarization: requiresNotarization ?? this.requiresNotarization, getBalances: getBalances ?? this.getBalances, - privKeyPolicy: privKeyPolicy ?? super.privKeyPolicy, + privKeyPolicy: privKeyPolicy ?? this.privKeyPolicy, nodes: nodes ?? this.nodes, ); } @@ -79,24 +80,39 @@ class TendermintActivationParams extends ActivationParams { 'get_balances': getBalances, 'nodes': nodes.map((e) => e.toJson()).toList(), 'tx_history': txHistory, + 'priv_key_policy': + (privKeyPolicy ?? const PrivateKeyPolicy.contextPrivKey()) + .pascalCaseName, }; } } // tendermint_token_activation_params.dart class TendermintTokenActivationParams extends ActivationParams { - TendermintTokenActivationParams({super.requiredConfirmations = 3}); + TendermintTokenActivationParams({ + super.requiredConfirmations, + super.privKeyPolicy, + }); - factory TendermintTokenActivationParams.fromJson(JsonMap json) { + factory TendermintTokenActivationParams.fromJson( + JsonMap json, { + PrivateKeyPolicy? privKeyPolicy, + }) { final base = ActivationParams.fromConfigJson(json); return TendermintTokenActivationParams( requiredConfirmations: base.requiredConfirmations ?? 3, + privKeyPolicy: privKeyPolicy ?? base.privKeyPolicy, ); } @override JsonMap toRpcParams() { - return {...super.toRpcParams()}; + return { + ...super.toRpcParams(), + 'priv_key_policy': + (privKeyPolicy ?? const PrivateKeyPolicy.contextPrivKey()) + .pascalCaseName, + }; } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart index ead67807..689a7730 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart @@ -49,4 +49,24 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { ), ); } + + // ETH Task Methods + Future enableEthInit({ + required String ticker, + required EthWithTokensActivationParams params, + }) { + return execute( + TaskEnableEthInit(rpcPass: rpcPass ?? '', ticker: ticker, params: params), + ); + } + + Future taskEthStatus(int taskId, [String? rpcPass]) { + return execute( + TaskStatusRequest( + taskId: taskId, + rpcPass: rpcPass, + method: 'task::enable_eth::status', + ), + ); + } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart new file mode 100644 index 00000000..bc2fc030 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart @@ -0,0 +1,40 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +class TaskEnableEthInit + extends BaseRequest { + TaskEnableEthInit({required this.ticker, required this.params, super.rpcPass}) + : super(method: 'task::enable_eth::init', mmrpc: '2.0'); + + final String ticker; + + @override + // ignore: overridden_fields + final EthWithTokensActivationParams params; + + @override + Map toJson() => { + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'ticker': ticker, ...params.toRpcParams()}, + }; + + @override + NewTaskResponse parseResponse(String responseBody) { + final json = jsonFromString(responseBody); + if (GeneralErrorResponse.isErrorResponse(json)) { + throw GeneralErrorResponse.parse(json); + } + return NewTaskResponse.parse(json); + } + + @override + NewTaskResponse parse(Map json) { + if (GeneralErrorResponse.isErrorResponse(json)) { + throw GeneralErrorResponse.parse(json); + } + return NewTaskResponse.parse(json); + } +} 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 8934bdab..51437110 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 @@ -17,6 +17,7 @@ export 'eth/enable_custom_erc20.dart'; export 'eth/enable_erc20.dart'; export 'eth/enable_eth_with_tokens.dart'; export 'eth/eth_rpc_extensions.dart'; +export 'eth/task_enable_eth_init.dart'; export 'hd_wallet/account_balance.dart'; export 'hd_wallet/get_new_address.dart'; export 'hd_wallet/get_new_address_task.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/activation/_activation.dart b/packages/komodo_defi_sdk/lib/src/activation/_activation.dart index 9dbb1869..33fa80c5 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/_activation.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/_activation.dart @@ -11,7 +11,9 @@ export 'protocol_strategies/bch_activation_strategy.dart'; export 'protocol_strategies/bch_with_tokens_batch_strategy.dart'; export 'protocol_strategies/custom_erc20_activation_strategy.dart'; export 'protocol_strategies/erc20_activation_strategy.dart'; -export 'protocol_strategies/eth_with_tokens_batch_strategy.dart'; +export 'protocol_strategies/eth_task_activation_strategy.dart'; +export 'protocol_strategies/eth_with_tokens_activation_strategy.dart'; + export 'protocol_strategies/protocol_error_handler.dart'; export 'protocol_strategies/qtum_activation_strategy.dart'; export 'protocol_strategies/slp_activation_strategy.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/activation/_activation_index.dart b/packages/komodo_defi_sdk/lib/src/activation/_activation_index.dart index 9dbb1869..80f74937 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/_activation_index.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/_activation_index.dart @@ -11,7 +11,8 @@ export 'protocol_strategies/bch_activation_strategy.dart'; export 'protocol_strategies/bch_with_tokens_batch_strategy.dart'; export 'protocol_strategies/custom_erc20_activation_strategy.dart'; export 'protocol_strategies/erc20_activation_strategy.dart'; -export 'protocol_strategies/eth_with_tokens_batch_strategy.dart'; +export 'protocol_strategies/eth_task_activation_strategy.dart'; +export 'protocol_strategies/eth_with_tokens_activation_strategy.dart'; export 'protocol_strategies/protocol_error_handler.dart'; export 'protocol_strategies/qtum_activation_strategy.dart'; export 'protocol_strategies/slp_activation_strategy.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart index 2d17c678..fe128aea 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_factory.dart @@ -4,6 +4,11 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Factory for creating the complete activation strategy stack class ActivationStrategyFactory { + /// Creates a complete activation strategy stack with all protocols + /// and returns a [SmartAssetActivator] instance. + /// [client] The [ApiClient] to use for RPC calls. + /// [privKeyPolicy] The [PrivateKeyPolicy] to use for private key management. + /// This is used for external wallet support. E.g. trezor, wallet connect, etc static SmartAssetActivator createStrategy( ApiClient client, PrivateKeyPolicy privKeyPolicy, @@ -14,11 +19,13 @@ class ActivationStrategyFactory { // BCH strategy needs to be before UTXO strategy to handle the special case // BchActivationStrategy(client), UtxoActivationStrategy(client, privKeyPolicy), - Erc20ActivationStrategy(client), + EthTaskActivationStrategy(client, privKeyPolicy), + EthWithTokensActivationStrategy(client, privKeyPolicy), + Erc20ActivationStrategy(client, privKeyPolicy), // SlpActivationStrategy(client), - TendermintActivationStrategy(client), + TendermintActivationStrategy(client, privKeyPolicy), QtumActivationStrategy(client, privKeyPolicy), - ZhtlcActivationStrategy(client), + ZhtlcActivationStrategy(client, privKeyPolicy), CustomErc20ActivationStrategy(client), ]), ); diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart index 7354319e..3fa1b70f 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart @@ -3,81 +3,80 @@ import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class Erc20ActivationStrategy extends ProtocolActivationStrategy { - const Erc20ActivationStrategy(super.client); + const Erc20ActivationStrategy(super.client, this.privKeyPolicy); + + /// The private key management policy to use for this strategy. + /// Used for external wallet support. + final PrivateKeyPolicy privKeyPolicy; @override Set get supportedProtocols => { - CoinSubClass.erc20, - CoinSubClass.bep20, - CoinSubClass.ftm20, - CoinSubClass.matic, - CoinSubClass.avx20, - CoinSubClass.hrc20, - CoinSubClass.moonbeam, - CoinSubClass.moonriver, - CoinSubClass.ethereumClassic, - CoinSubClass.ubiq, - CoinSubClass.krc20, - CoinSubClass.ewt, - CoinSubClass.hecoChain, - CoinSubClass.rskSmartBitcoin, - CoinSubClass.arbitrum, - }; + CoinSubClass.erc20, + CoinSubClass.bep20, + CoinSubClass.ftm20, + CoinSubClass.matic, + CoinSubClass.avx20, + CoinSubClass.hrc20, + CoinSubClass.moonbeam, + CoinSubClass.moonriver, + CoinSubClass.ethereumClassic, + CoinSubClass.ubiq, + CoinSubClass.krc20, + CoinSubClass.ewt, + CoinSubClass.hecoChain, + CoinSubClass.rskSmartBitcoin, + CoinSubClass.arbitrum, + }; + + @override + bool get supportsBatchActivation => false; @override - bool get supportsBatchActivation => true; + bool canHandle(Asset asset) { + // Use erc20 activation for token assets (not platform assets, not trezor) + final isTokenAsset = asset.id.parentId != null; + return isTokenAsset && + privKeyPolicy != const PrivateKeyPolicy.trezor() && + super.canHandle(asset); + } @override Stream activate( Asset asset, [ List? children, ]) async* { - final isPlatformAsset = asset.id.parentId == null; - if (!isPlatformAsset && children?.isNotEmpty == true) { - throw StateError('Child assets cannot perform batch activation'); + if (children?.isNotEmpty == true) { + throw StateError('Token assets cannot perform batch activation'); } yield ActivationProgress( - status: 'Activating ${asset.id.name}...', + status: 'Activating ${asset.id.name} token...', progressDetails: ActivationProgressDetails( currentStep: 'initialization', stepCount: 2, additionalInfo: { - 'assetType': isPlatformAsset ? 'platform' : 'token', + 'assetType': 'token', 'protocol': asset.protocol.subClass.formatted, }, ), ); try { - if (isPlatformAsset) { - await client.rpc.erc20.enableEthWithTokens( - ticker: asset.id.id, - params: EthWithTokensActivationParams.fromJson(asset.protocol.config) - .copyWith( - erc20Tokens: - children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? - [], - txHistory: true, - ), - ); - } else { - await client.rpc.erc20.enableErc20( - ticker: asset.id.id, - activationParams: Erc20ActivationParams.fromJsonConfig( - asset.protocol.config, - ), - ); - } + await client.rpc.erc20.enableErc20( + ticker: asset.id.id, + activationParams: Erc20ActivationParams.fromJsonConfig( + asset.protocol.config, + ), + ); yield ActivationProgress.success( details: ActivationProgressDetails( currentStep: 'complete', stepCount: 2, additionalInfo: { - 'activatedChain': asset.id.name, + 'activatedToken': asset.id.name, 'activationTime': DateTime.now().toIso8601String(), - 'childCount': children?.length ?? 0, + 'method': 'enableErc20', }, ), ); diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart new file mode 100644 index 00000000..f24807c5 --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart @@ -0,0 +1,215 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/activation/_activation.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class EthTaskActivationStrategy extends ProtocolActivationStrategy { + const EthTaskActivationStrategy(super.client, this.privKeyPolicy); + + /// The private key management policy to use for this strategy. + /// Used for external wallet support. + final PrivateKeyPolicy privKeyPolicy; + + @override + Set get supportedProtocols => { + CoinSubClass.erc20, + CoinSubClass.bep20, + CoinSubClass.ftm20, + CoinSubClass.matic, + CoinSubClass.avx20, + CoinSubClass.hrc20, + CoinSubClass.moonbeam, + CoinSubClass.moonriver, + CoinSubClass.ethereumClassic, + CoinSubClass.ubiq, + CoinSubClass.krc20, + CoinSubClass.ewt, + CoinSubClass.hecoChain, + CoinSubClass.rskSmartBitcoin, + CoinSubClass.arbitrum, + }; + + @override + bool get supportsBatchActivation => true; + + @override + bool canHandle(Asset asset) { + // Use task-based activation for Trezor private key policy + return privKeyPolicy == const PrivateKeyPolicy.trezor() && + super.canHandle(asset); + } + + @override + Stream activate( + Asset asset, [ + List? children, + ]) async* { + final protocol = asset.protocol as Erc20Protocol; + + yield ActivationProgress( + status: 'Starting ${asset.id.name} activation...', + progressDetails: ActivationProgressDetails( + currentStep: 'initialization', + stepCount: 5, + additionalInfo: { + 'chainType': protocol.subClass.formatted, + 'contractAddress': protocol.contractAddress, + 'nodes': protocol.nodes.length, + }, + ), + ); + + try { + yield const ActivationProgress( + status: 'Validating protocol configuration...', + progressPercentage: 20, + progressDetails: ActivationProgressDetails( + currentStep: 'validation', + stepCount: 5, + ), + ); + + final taskResponse = await client.rpc.erc20.enableEthInit( + ticker: asset.id.id, + params: EthWithTokensActivationParams.fromJson( + asset.protocol.config, + ).copyWith( + erc20Tokens: + children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? + [], + txHistory: true, + privKeyPolicy: privKeyPolicy, + ), + ); + + yield ActivationProgress( + status: 'Establishing network connections...', + progressPercentage: 40, + progressDetails: ActivationProgressDetails( + currentStep: 'connection', + stepCount: 5, + additionalInfo: { + 'nodes': protocol.requiredServers.toJsonRequest(), + 'protocolType': protocol.subClass.formatted, + 'tokenCount': children?.length ?? 0, + }, + ), + ); + + var isComplete = false; + while (!isComplete) { + final status = await client.rpc.erc20.taskEthStatus( + taskResponse.taskId, + ); + + if (status.isCompleted) { + if (status.status == 'Ok') { + yield ActivationProgress.success( + details: ActivationProgressDetails( + currentStep: 'complete', + stepCount: 5, + additionalInfo: { + 'activatedChain': asset.id.name, + 'activationTime': DateTime.now().toIso8601String(), + 'childCount': children?.length ?? 0, + }, + ), + ); + } else { + yield ActivationProgress( + status: 'Activation failed: ${status.details}', + errorMessage: status.details, + isComplete: true, + progressDetails: ActivationProgressDetails( + currentStep: 'error', + stepCount: 5, + errorCode: 'ETH_TASK_ACTIVATION_ERROR', + errorDetails: status.details, + ), + ); + } + isComplete = true; + } else { + final progress = _parseEthStatus(status.status); + yield ActivationProgress( + status: progress.status, + progressPercentage: progress.percentage, + progressDetails: ActivationProgressDetails( + currentStep: progress.step, + stepCount: 5, + additionalInfo: progress.info, + ), + ); + await Future.delayed(const Duration(milliseconds: 500)); + } + } + } catch (e, stack) { + yield ActivationProgress( + status: 'Activation failed', + errorMessage: e.toString(), + isComplete: true, + progressDetails: ActivationProgressDetails( + currentStep: 'error', + stepCount: 5, + errorCode: 'ETH_TASK_ACTIVATION_ERROR', + errorDetails: e.toString(), + stackTrace: stack.toString(), + ), + ); + } + } + + ({String status, double percentage, String step, Map info}) + _parseEthStatus(String status) { + switch (status) { + case 'ActivatingCoin': + return ( + status: 'Activating platform coin...', + percentage: 60, + step: 'coin_activation', + info: {'activationType': 'platform'}, + ); + case 'RequestingWalletBalance': + return ( + status: 'Requesting wallet balance...', + percentage: 70, + step: 'balance_request', + info: {'dataType': 'balance'}, + ); + case 'ActivatingTokens': + return ( + status: 'Activating ERC20 tokens...', + percentage: 80, + step: 'token_activation', + info: {'activationType': 'tokens'}, + ); + case 'Finishing': + return ( + status: 'Finalizing activation...', + percentage: 90, + step: 'finalization', + info: {'stage': 'completion'}, + ); + case 'WaitingForTrezorToConnect': + return ( + status: 'Waiting for Trezor device...', + percentage: 50, + step: 'trezor_connection', + info: {'deviceType': 'Trezor', 'action': 'connect'}, + ); + case 'FollowHwDeviceInstructions': + return ( + status: 'Follow instructions on hardware device', + percentage: 55, + step: 'hardware_interaction', + info: {'deviceType': 'Hardware', 'action': 'follow_instructions'}, + ); + default: + return ( + status: 'Processing activation...', + percentage: 95, + step: 'processing', + info: {'status': status}, + ); + } + } +} diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart new file mode 100644 index 00000000..1be505b2 --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart @@ -0,0 +1,139 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/activation/_activation.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { + const EthWithTokensActivationStrategy(super.client, this.privKeyPolicy); + + /// The private key management policy to use for this strategy. + /// Used for external wallet support. + final PrivateKeyPolicy privKeyPolicy; + + @override + Set get supportedProtocols => { + CoinSubClass.erc20, + CoinSubClass.bep20, + CoinSubClass.ftm20, + CoinSubClass.matic, + CoinSubClass.avx20, + CoinSubClass.hrc20, + CoinSubClass.moonbeam, + CoinSubClass.moonriver, + CoinSubClass.ethereumClassic, + CoinSubClass.ubiq, + CoinSubClass.krc20, + CoinSubClass.ewt, + CoinSubClass.hecoChain, + CoinSubClass.rskSmartBitcoin, + CoinSubClass.arbitrum, + }; + + @override + bool get supportsBatchActivation => true; + + @override + bool canHandle(Asset asset) { + // Use eth-with-tokens for platform assets (not trezor) + final isPlatformAsset = asset.id.parentId == null; + return isPlatformAsset && + privKeyPolicy != const PrivateKeyPolicy.trezor() && + super.canHandle(asset); + } + + @override + Stream activate( + Asset asset, [ + List? children, + ]) async* { + if (children?.isNotEmpty == true) { + yield ActivationProgress( + status: + 'Activating ${asset.id.name} with ${children!.length} tokens...', + progressDetails: ActivationProgressDetails( + currentStep: 'initialization', + stepCount: 3, + additionalInfo: { + 'assetType': 'platform', + 'protocol': asset.protocol.subClass.formatted, + 'tokenCount': children.length, + }, + ), + ); + } else { + yield ActivationProgress( + status: 'Activating ${asset.id.name}...', + progressDetails: ActivationProgressDetails( + currentStep: 'initialization', + stepCount: 3, + additionalInfo: { + 'assetType': 'platform', + 'protocol': asset.protocol.subClass.formatted, + }, + ), + ); + } + + try { + yield ActivationProgress( + status: 'Configuring platform activation...', + progressPercentage: 33, + progressDetails: ActivationProgressDetails( + currentStep: 'configuration', + stepCount: 3, + additionalInfo: { + 'method': 'enableEthWithTokens', + 'tokenCount': children?.length ?? 0, + }, + ), + ); + + await client.rpc.erc20.enableEthWithTokens( + ticker: asset.id.id, + params: EthWithTokensActivationParams.fromJson( + asset.protocol.config, + ).copyWith( + erc20Tokens: + children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? + [], + txHistory: true, + privKeyPolicy: privKeyPolicy, + ), + ); + + yield ActivationProgress( + status: 'Finalizing activation...', + progressPercentage: 66, + progressDetails: ActivationProgressDetails( + currentStep: 'finalization', + stepCount: 3, + ), + ); + + yield ActivationProgress.success( + details: ActivationProgressDetails( + currentStep: 'complete', + stepCount: 3, + additionalInfo: { + 'activatedChain': asset.id.name, + 'activationTime': DateTime.now().toIso8601String(), + 'childCount': children?.length ?? 0, + 'method': 'enableEthWithTokens', + }, + ), + ); + } catch (e, stack) { + yield ActivationProgress( + status: 'Activation failed', + errorMessage: e.toString(), + isComplete: true, + progressDetails: ActivationProgressDetails( + currentStep: 'error', + stepCount: 3, + errorCode: 'ETH_WITH_TOKENS_ACTIVATION_ERROR', + errorDetails: e.toString(), + stackTrace: stack.toString(), + ), + ); + } + } +} diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_batch_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_batch_strategy.dart deleted file mode 100644 index 6bbcb26e..00000000 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_batch_strategy.dart +++ /dev/null @@ -1,35 +0,0 @@ -// import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -// import 'package:komodo_defi_sdk/src/activation/base_strategies/batch_activation.dart'; -// import 'package:komodo_defi_sdk/src/assets/asset_manager.dart'; -// import 'package:komodo_defi_types/komodo_defi_types.dart'; - -// /// Handles activation of ETH and ERC20 tokens together -// class EthWithTokensBatchStrategy implements BatchActivationStrategy { -// @override -// Future activate( -// ApiClient client, -// Asset parent, -// List children, -// ) async { -// // Validate parent is ETH -// if (parent.protocol is! Erc20Protocol) { -// throw ArgumentError('Parent must be ETH'); -// } - -// // Convert children to TokensRequest format -// final tokenRequests = -// children.map((child) => TokensRequest(ticker: child.id.id)).toList(); - -// // Create ETH activation params with tokens -// final params = (parent -// .activationStrategy(dependencies: children) -// .activationParams as EthActivationParams) -// .copyWith(erc20Tokens: tokenRequests); - -// // Enable ETH with tokens -// await client.rpc.erc20.enableEthWithTokens( -// ticker: parent.id.id, -// params: params, -// ); -// } -// } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart index 9f4b80ea..44474931 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart @@ -5,6 +5,8 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; class QtumActivationStrategy extends ProtocolActivationStrategy { const QtumActivationStrategy(super.client, this.privKeyPolicy); + /// The private key management policy to use for this strategy. + /// Used for external wallet support. final PrivateKeyPolicy privKeyPolicy; @override diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart index 90b89475..ab9e1a6b 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart @@ -3,13 +3,15 @@ import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class TendermintActivationStrategy extends ProtocolActivationStrategy { - const TendermintActivationStrategy(super.client); + const TendermintActivationStrategy(super.client, this.privKeyPolicy); + + final PrivateKeyPolicy privKeyPolicy; @override Set get supportedProtocols => { - CoinSubClass.tendermint, - CoinSubClass.tendermintToken, - }; + CoinSubClass.tendermint, + CoinSubClass.tendermintToken, + }; @override bool get supportsBatchActivation => true; @@ -65,14 +67,14 @@ class TendermintActivationStrategy extends ProtocolActivationStrategy { await client.rpc.tendermint.enableTendermintWithAssets( ticker: asset.id.id, params: TendermintActivationParams.fromJson(protocol.config).copyWith( - tokensParams: children - ?.map( - (child) => TokensRequest(ticker: child.id.id), - ) + tokensParams: + children + ?.map((child) => TokensRequest(ticker: child.id.id)) .toList() ?? [], getBalances: true, txHistory: true, + privKeyPolicy: privKeyPolicy, ), ); } else { @@ -87,7 +89,7 @@ class TendermintActivationStrategy extends ProtocolActivationStrategy { await client.rpc.tendermint.enableTendermintToken( ticker: asset.id.id, - params: TendermintTokenActivationParams(), + params: TendermintTokenActivationParams(privKeyPolicy: privKeyPolicy), ); } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart index 7d947b09..3b3aacb0 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart @@ -5,6 +5,8 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; class UtxoActivationStrategy extends ProtocolActivationStrategy { const UtxoActivationStrategy(super.client, this.privKeyPolicy); + /// The private key management policy to use for this strategy. + /// Used for external wallet support. final PrivateKeyPolicy privKeyPolicy; @override diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart index 9bd40b0f..d5682fb4 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart @@ -6,7 +6,9 @@ import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class ZhtlcActivationStrategy extends ProtocolActivationStrategy { - const ZhtlcActivationStrategy(super.client); + const ZhtlcActivationStrategy(super.client, this.privKeyPolicy); + + final PrivateKeyPolicy privKeyPolicy; @override Set get supportedProtocols => {CoinSubClass.zhtlc}; @@ -40,11 +42,13 @@ class ZhtlcActivationStrategy extends ProtocolActivationStrategy { try { final protocol = asset.protocol as ZhtlcProtocol; - final params = - ActivationParams.fromConfigJson(protocol.config).genericCopyWith( + final params = ActivationParams.fromConfigJson( + protocol.config, + ).genericCopyWith( scanBlocksPerIteration: 200, scanIntervalMs: 200, zcashParamsPath: protocol.zcashParamsPath, + privKeyPolicy: privKeyPolicy, ); // Setup parameters @@ -64,10 +68,7 @@ class ZhtlcActivationStrategy extends ProtocolActivationStrategy { // Initialize task final taskResponse = await client.rpc.task.execute( - TaskEnableZhtlcInit( - params: params, - ticker: asset.id.id, - ), + TaskEnableZhtlcInit(params: params, ticker: asset.id.id), ); var isComplete = false; 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 40d90180..b450f539 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'dart:collection'; + import 'package:flutter/foundation.dart' show ValueGetter; import 'package:komodo_coins/komodo_coins.dart'; import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/_internal_exports.dart'; import 'package:komodo_defi_sdk/src/sdk/komodo_defi_sdk_config.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -37,6 +39,9 @@ typedef AssetIdMap = SplayTreeMap; /// // Get all activated assets /// final activeAssets = await assetManager.getActivatedAssets(); /// ``` +/// +/// The manager listens to authentication changes to keep the available asset +/// list in sync with the active wallet's capabilities. class AssetManager implements IAssetProvider { /// Creates a new instance of AssetManager. /// @@ -48,7 +53,9 @@ class AssetManager implements IAssetProvider { this._config, this._customAssetHistory, this._activationManager, - ); + ) { + _authSubscription = _auth.authStateChanges.listen(_handleAuthStateChange); + } final ApiClient _client; final KomodoDefiLocalAuth _auth; @@ -56,6 +63,9 @@ class AssetManager implements IAssetProvider { final CustomAssetHistoryStorage _customAssetHistory; final KomodoCoins _coins = KomodoCoins(); late final AssetIdMap _orderedCoins; + StreamSubscription? _authSubscription; + bool _isDisposed = false; + AssetFilterStrategy? _currentFilterStrategy; /// NB: This cannot be used during initialization. This is a workaround /// to publicly expose the activation manager's activation methods. @@ -80,11 +90,29 @@ class AssetManager implements IAssetProvider { return keyA.toString().compareTo(keyB.toString()); }); - _orderedCoins.addAll(_coins.all); + _refreshCoins(const NoAssetFilterStrategy()); await _initializeCustomTokens(); } + void _refreshCoins(AssetFilterStrategy strategy) { + if (_currentFilterStrategy?.strategyId == strategy.strategyId) return; + _orderedCoins + ..clear() + ..addAll(_coins.filteredAssets(strategy)); + _currentFilterStrategy = strategy; + } + + /// Applies a new [strategy] for filtering available assets. + /// + /// This is called whenever the authentication state changes so the + /// visible asset list always matches the capabilities of the active wallet. + void setFilterStrategy(AssetFilterStrategy strategy) { + if (_coins.isInitialized) { + _refreshCoins(strategy); + } + } + Future _initializeCustomTokens() async { final user = await _auth.currentUser; if (user != null) { @@ -97,6 +125,28 @@ class AssetManager implements IAssetProvider { } } + /// Reacts to authentication changes by updating the active asset filter. + /// + /// When a hardware wallet such as Trezor is connected we limit the list of + /// available assets to only those explicitly supported by that wallet. + void _handleAuthStateChange(KdfUser? user) { + if (_isDisposed) return; + + final isTrezor = + user?.walletId.authOptions.privKeyPolicy == + const PrivateKeyPolicy.trezor(); + + // Trezor does not support all assets yet, so we apply a filter here + // to only show assets that are compatible with Trezor. + // WalletConnect and Metamask will require similar handling in the future. + final strategy = + isTrezor + ? const TrezorAssetFilterStrategy() + : const NoAssetFilterStrategy(); + + setFilterStrategy(strategy); + } + /// Returns an asset by its [AssetId], if available. /// /// Returns null if no matching asset is found. @@ -199,6 +249,7 @@ class AssetManager implements IAssetProvider { /// /// This is called automatically by the SDK when disposing. Future dispose() async { - // No cleanup needed for now + _isDisposed = true; + await _authSubscription?.cancel(); } } diff --git a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart index a9e8177f..fb0abf14 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart @@ -275,3 +275,21 @@ enum CoinSubClass { } } } + +const Set evmCoinSubClasses = { + CoinSubClass.avx20, + CoinSubClass.bep20, + CoinSubClass.ftm20, + CoinSubClass.matic, + CoinSubClass.hrc20, + CoinSubClass.arbitrum, + CoinSubClass.moonriver, + CoinSubClass.moonbeam, + CoinSubClass.ethereumClassic, + CoinSubClass.ubiq, + CoinSubClass.krc20, + CoinSubClass.ewt, + CoinSubClass.hecoChain, + CoinSubClass.rskSmartBitcoin, + CoinSubClass.erc20, +}; diff --git a/packages/komodo_wallet_build_transformer/pubspec.yaml b/packages/komodo_wallet_build_transformer/pubspec.yaml index 1df46214..f9d6ff68 100644 --- a/packages/komodo_wallet_build_transformer/pubspec.yaml +++ b/packages/komodo_wallet_build_transformer/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: crypto: ^3.0.3 # dart.dev html: ^0.15.4 http: ^1.4.0 # dart.dev - logging: ^1.2.0 # dart.dev + logging: ^1.3.0 # dart.dev path: ^1.9.1 dev_dependencies: