Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/komodo_coins/lib/komodo_coins.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// TODO! Library description
library komodo_coins;

export 'src/asset_filter.dart';
export 'src/komodo_coins_base.dart';

/// A Calculator.
Expand Down
69 changes: 69 additions & 0 deletions packages/komodo_coins/lib/src/asset_filter.dart
Original file line number Diff line number Diff line change
@@ -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<Object?> 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);
}
33 changes: 31 additions & 2 deletions packages/komodo_coins/lib/src/komodo_coins_base.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,6 +18,7 @@ class KomodoCoins {
}

Map<AssetId, Asset>? _assets;
final Map<String, Map<AssetId, Asset>> _filterCache = {};

@mustCallSuper
Future<void> init() async {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -111,6 +115,31 @@ class KomodoCoins {
coinData.valueOrNull<String>('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<AssetId, Asset> 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 = <AssetId, Asset>{};
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
Expand Down
76 changes: 76 additions & 0 deletions packages/komodo_coins/test/asset_filter_test.dart
Original file line number Diff line number Diff line change
@@ -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 = <AssetId, Asset>{};
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<String, dynamic>.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<String, dynamic>.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);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ 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(),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ class TaskEnableEthInit
}
return NewTaskResponse.parse(json);
}

@override
NewTaskResponse parse(Map<String, dynamic> json) {
if (GeneralErrorResponse.isErrorResponse(json)) {
throw GeneralErrorResponse.parse(json);
}
return NewTaskResponse.parse(json);
}
}
57 changes: 54 additions & 3 deletions packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,6 +39,9 @@ typedef AssetIdMap = SplayTreeMap<AssetId, Asset>;
/// // 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.
///
Expand All @@ -48,14 +53,19 @@ class AssetManager implements IAssetProvider {
this._config,
this._customAssetHistory,
this._activationManager,
);
) {
_authSubscription = _auth.authStateChanges.listen(_handleAuthStateChange);
}

final ApiClient _client;
final KomodoDefiLocalAuth _auth;
final KomodoDefiSdkConfig _config;
final CustomAssetHistoryStorage _customAssetHistory;
final KomodoCoins _coins = KomodoCoins();
late final AssetIdMap _orderedCoins;
StreamSubscription<KdfUser?>? _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.
Expand All @@ -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<void> _initializeCustomTokens() async {
final user = await _auth.currentUser;
if (user != null) {
Expand All @@ -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.
Expand Down Expand Up @@ -199,6 +249,7 @@ class AssetManager implements IAssetProvider {
///
/// This is called automatically by the SDK when disposing.
Future<void> dispose() async {
// No cleanup needed for now
_isDisposed = true;
await _authSubscription?.cancel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,21 @@ enum CoinSubClass {
}
}
}

const Set<CoinSubClass> 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,
};
Loading