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
6 changes: 3 additions & 3 deletions lib/app_config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ const String coinsAssetsPath = 'packages/komodo_defi_framework/assets';
final Uri discordSupportChannelUrl = Uri.parse(
'mailto:info@gleec.com?subject=GLEEC%20Wallet%20Support',
);
final Uri discordInviteUrl = Uri.parse(
'https://www.gleec.com/contact',
);
final Uri discordInviteUrl = Uri.parse('https://www.gleec.com/contact');

/// Const to define if Bitrefill integration is enabled in the app.
const bool isBitrefillIntegrationEnabled = false;
Expand Down Expand Up @@ -79,6 +77,7 @@ Map<String, int> priorityCoinsAbbrMap = {
'USDT-ERC20': 80,
'USDT-PLG20': 80,
'USDT-BEP20': 80,
'USDT-TRC20': 80,

// Rank 4: XRP (~$145 billion)
'XRP': 70,
Expand Down Expand Up @@ -119,6 +118,7 @@ const List<String> unauthenticatedUserPriorityTickers = [
'BTC',
'KMD',
'ETH',
'TRX',
'BNB',
'LTC',
'DASH',
Expand Down
11 changes: 11 additions & 0 deletions lib/bloc/coins_bloc/asset_coin_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ extension AssetCoinExtension on Asset {
extension CoinTypeExtension on CoinSubClass {
CoinType toCoinType() {
switch (this) {
case CoinSubClass.trx:
return CoinType.trx;
case CoinSubClass.trc20:
return CoinType.trc20;
case CoinSubClass.base:
return CoinType.base20;
case CoinSubClass.ftm20:
Expand Down Expand Up @@ -128,6 +132,9 @@ extension CoinTypeExtension on CoinSubClass {
switch (this) {
case CoinSubClass.base:
return true;
case CoinSubClass.trx:
case CoinSubClass.trc20:
return false;
case CoinSubClass.avx20:
case CoinSubClass.bep20:
case CoinSubClass.ftm20:
Expand Down Expand Up @@ -158,6 +165,10 @@ extension CoinTypeExtension on CoinSubClass {
extension CoinSubClassExtension on CoinType {
CoinSubClass toCoinSubClass() {
switch (this) {
case CoinType.trx:
return CoinSubClass.trx;
case CoinType.trc20:
return CoinSubClass.trc20;
case CoinType.base20:
return CoinSubClass.base;
case CoinType.ftm20:
Expand Down
69 changes: 69 additions & 0 deletions lib/bloc/coins_bloc/coins_repo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:web_dex/generated/codegen_loader.g.dart';
import 'package:web_dex/mm2/mm2.dart';
import 'package:web_dex/mm2/mm2_api/rpc/base.dart';
import 'package:web_dex/mm2/mm2_api/rpc/bloc_response.dart';
import 'package:web_dex/mm2/mm2_api/rpc/disable_coin/disable_coin_req.dart';
import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_errors.dart';
import 'package:web_dex/mm2/mm2_api/rpc/withdraw/withdraw_request.dart';
import 'package:web_dex/model/cex_price.dart';
Expand Down Expand Up @@ -705,6 +706,74 @@ class CoinsRepo {
_invalidateActivatedAssetsCache();
}

/// Performs a full rollback for preview-only asset activations.
///
/// Unlike [deactivateCoinsSync], this disables the assets in MM2 so
/// temporary preview activations do not remain active for the rest of the
/// session. This should only be used for short-lived preview flows where a
/// real rollback is required.
Future<void> rollbackPreviewAssets(
Iterable<Asset> assets, {
Set<AssetId> deleteCustomTokens = const {},
Set<AssetId> removeWalletMetadataAssets = const {},
bool notifyListeners = false,
}) async {
final uniqueAssets = Map<AssetId, Asset>.fromEntries(
assets.map((asset) => MapEntry(asset.id, asset)),
);
if (uniqueAssets.isEmpty) {
return;
}

final orderedAssets = uniqueAssets.values.toList()
..sort((a, b) {
final aPriority = a.id.parentId == null ? 1 : 0;
final bPriority = b.id.parentId == null ? 1 : 0;
return aPriority.compareTo(bPriority);
});

for (final asset in orderedAssets) {
await _balanceWatchers[asset.id]?.cancel();
_balanceWatchers.remove(asset.id);

try {
if (await isAssetActivated(asset.id, forceRefresh: true)) {
await _mm2.call(DisableCoinReq(coin: asset.id.id));
}
} catch (e, s) {
_log.warning('Failed to disable preview asset ${asset.id.id}', e, s);
}

if (notifyListeners) {
_broadcastAsset(asset.toCoin().copyWith(state: CoinState.inactive));
}
}

if (removeWalletMetadataAssets.isNotEmpty) {
try {
await _kdfSdk.removeActivatedCoins(
removeWalletMetadataAssets.map((assetId) => assetId.id).toList(),
);
} catch (e, s) {
_log.warning(
'Failed to remove preview assets from wallet metadata',
e,
s,
);
}
}

for (final assetId in deleteCustomTokens) {
try {
await _kdfSdk.deleteCustomToken(assetId);
} catch (e, s) {
_log.warning('Failed to delete preview custom token $assetId', e, s);
}
}

_invalidateActivatedAssetsCache();
}

double? getUsdPriceByAmount(String amount, String coinAbbr) {
final Coin? coin = getCoin(coinAbbr);
final double? parsedAmount = double.tryParse(amount);
Expand Down
165 changes: 146 additions & 19 deletions lib/bloc/custom_token_import/bloc/custom_token_import_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,55 @@ import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_event.
import 'package:web_dex/bloc/custom_token_import/bloc/custom_token_import_state.dart';
import 'package:web_dex/bloc/custom_token_import/data/custom_token_import_repository.dart';
import 'package:web_dex/model/coin_type.dart';
import 'package:web_dex/model/kdf_auth_metadata_extension.dart';
import 'package:web_dex/shared/utils/extensions/kdf_user_extensions.dart';

class _CustomTokenPreviewSession {
const _CustomTokenPreviewSession({
required this.platformAsset,
required this.wasPlatformAlreadyActivated,
required this.wasPlatformAlreadyInWalletMetadata,
this.tokenAsset,
this.wasTokenAlreadyActivated = false,
this.wasTokenAlreadyInWalletMetadata = false,
this.wasTokenAlreadyKnown = false,
});

final Asset platformAsset;
final bool wasPlatformAlreadyActivated;
final bool wasPlatformAlreadyInWalletMetadata;
final Asset? tokenAsset;
final bool wasTokenAlreadyActivated;
final bool wasTokenAlreadyInWalletMetadata;
final bool wasTokenAlreadyKnown;

_CustomTokenPreviewSession copyWith({
Asset? platformAsset,
bool? wasPlatformAlreadyActivated,
bool? wasPlatformAlreadyInWalletMetadata,
Asset? Function()? tokenAsset,
bool? wasTokenAlreadyActivated,
bool? wasTokenAlreadyInWalletMetadata,
bool? wasTokenAlreadyKnown,
}) {
return _CustomTokenPreviewSession(
platformAsset: platformAsset ?? this.platformAsset,
wasPlatformAlreadyActivated:
wasPlatformAlreadyActivated ?? this.wasPlatformAlreadyActivated,
wasPlatformAlreadyInWalletMetadata:
wasPlatformAlreadyInWalletMetadata ??
this.wasPlatformAlreadyInWalletMetadata,
tokenAsset: tokenAsset != null ? tokenAsset() : this.tokenAsset,
wasTokenAlreadyActivated:
wasTokenAlreadyActivated ?? this.wasTokenAlreadyActivated,
wasTokenAlreadyInWalletMetadata:
wasTokenAlreadyInWalletMetadata ??
this.wasTokenAlreadyInWalletMetadata,
wasTokenAlreadyKnown: wasTokenAlreadyKnown ?? this.wasTokenAlreadyKnown,
);
}
}

class CustomTokenImportBloc
extends Bloc<CustomTokenImportEvent, CustomTokenImportState> {
CustomTokenImportBloc(
Expand All @@ -36,19 +83,21 @@ class CustomTokenImportBloc
final KomodoDefiSdk _sdk;
final AnalyticsBloc _analyticsBloc;
final _log = Logger('CustomTokenImportBloc');
_CustomTokenPreviewSession? _previewSession;

void _onResetFormStatus(
Future<void> _onResetFormStatus(
ResetFormStatusEvent event,
Emitter<CustomTokenImportState> emit,
) {
) async {
await _rollbackPreviewIfNeeded();

final availableCoinTypes = CoinType.values.map(
(CoinType type) => type.toCoinSubClass(),
);
final items = CoinSubClass.values.where((CoinSubClass type) {
final isEvm = type.isEvmProtocol();
final isAvailable = availableCoinTypes.contains(type);
final isSupported = _repository.getNetworkApiName(type) != null;
return isEvm && isAvailable && isSupported;
return isAvailable && isSupported;
}).toList()..sort((a, b) => a.name.compareTo(b.name));

emit(
Expand All @@ -57,7 +106,7 @@ class CustomTokenImportBloc
formErrorMessage: '',
importStatus: FormStatus.initial,
importErrorMessage: '',
evmNetworks: items,
supportedNetworks: items,
),
);
}
Expand Down Expand Up @@ -85,21 +134,46 @@ class CustomTokenImportBloc
) async {
emit(state.copyWith(formStatus: FormStatus.submitting));

Asset? tokenData;
try {
final networkAsset = _sdk.getSdkAsset(state.network.ticker);
final walletCoinIds = (await _sdk.getWalletCoinIds()).toSet();
final platformAsset = _sdk.getSdkAsset(state.network.ticker);
final wasPlatformAlreadyActivated = await _coinsRepo.isAssetActivated(
platformAsset.id,
);
_previewSession = _CustomTokenPreviewSession(
platformAsset: platformAsset,
wasPlatformAlreadyActivated: wasPlatformAlreadyActivated,
wasPlatformAlreadyInWalletMetadata: walletCoinIds.contains(
platformAsset.id.id,
),
);

// Network (parent) asset must be active before attempting to fetch the
// custom token data
await _coinsRepo.activateAssetsSync(
[networkAsset],
[platformAsset],
notifyListeners: false,
addToWalletMetadata: false,
);

tokenData = await _repository.fetchCustomToken(
networkAsset.id,
state.address,
final tokenData = await _repository.fetchCustomToken(
network: state.network,
platformAsset: platformAsset,
address: state.address,
);
final wasTokenAlreadyKnown = _sdk.assets.available.containsKey(
tokenData.id,
);
final wasTokenAlreadyActivated = await _coinsRepo.isAssetActivated(
tokenData.id,
);
_previewSession = _previewSession?.copyWith(
tokenAsset: () => tokenData,
wasTokenAlreadyActivated: wasTokenAlreadyActivated,
wasTokenAlreadyInWalletMetadata: walletCoinIds.contains(
tokenData.id.id,
),
wasTokenAlreadyKnown: wasTokenAlreadyKnown,
);
await _coinsRepo.activateAssetsSync(
[tokenData],
Expand Down Expand Up @@ -130,19 +204,14 @@ class CustomTokenImportBloc
);
} catch (e, s) {
_log.severe('Error fetching custom token', e, s);
await _rollbackPreviewIfNeeded();
emit(
state.copyWith(
formStatus: FormStatus.failure,
tokenData: () => null,
formErrorMessage: e.toString(),
formErrorMessage: _formatImportError(e),
),
);
} finally {
if (tokenData != null) {
// Activate to get balance, then deactivate to avoid confusion if the user
// does not proceed with the import (exits the dialog).
await _coinsRepo.deactivateCoinsSync([tokenData.toCoin()]);
}
}
}

Expand Down Expand Up @@ -177,6 +246,7 @@ class CustomTokenImportBloc

try {
await _repository.importCustomToken(state.coin!);
_previewSession = null;

final walletType = (await _sdk.auth.currentUser)?.type ?? '';
_analyticsBloc.logEvent(
Expand All @@ -198,14 +268,71 @@ class CustomTokenImportBloc
emit(
state.copyWith(
importStatus: FormStatus.failure,
importErrorMessage: e.toString(),
importErrorMessage: _formatImportError(e),
),
);
}
}

String _formatImportError(Object error) {
return switch (error) {
final CustomTokenConflictException e => e.message,
final UnsupportedCustomTokenNetworkException e => e.message,
_ => error.toString(),
};
}

Future<void> _rollbackPreviewIfNeeded() async {
final previewSession = _previewSession;
_previewSession = null;

if (previewSession == null) {
return;
}

final rollbackAssets = <Asset>[];
final deleteCustomTokens = <AssetId>{};
final removeWalletMetadataAssets = <AssetId>{};

final tokenAsset = previewSession.tokenAsset;
if (tokenAsset != null && !previewSession.wasTokenAlreadyActivated) {
rollbackAssets.add(tokenAsset);
if (!previewSession.wasTokenAlreadyInWalletMetadata) {
removeWalletMetadataAssets.add(tokenAsset.id);
}
if (!previewSession.wasTokenAlreadyKnown &&
!previewSession.wasTokenAlreadyInWalletMetadata) {
deleteCustomTokens.add(tokenAsset.id);
}
Comment thread
CharlVS marked this conversation as resolved.
}

if (!previewSession.wasPlatformAlreadyActivated) {
rollbackAssets.add(previewSession.platformAsset);
if (!previewSession.wasPlatformAlreadyInWalletMetadata) {
removeWalletMetadataAssets.add(previewSession.platformAsset.id);
}
}

if (rollbackAssets.isEmpty &&
deleteCustomTokens.isEmpty &&
removeWalletMetadataAssets.isEmpty) {
return;
}

try {
await _coinsRepo.rollbackPreviewAssets(
rollbackAssets,
deleteCustomTokens: deleteCustomTokens,
removeWalletMetadataAssets: removeWalletMetadataAssets,
);
} catch (e, s) {
_log.warning('Failed to rollback preview activation state', e, s);
}
}

@override
Future<void> close() async {
await _rollbackPreviewIfNeeded();
_repository.dispose();
await super.close();
}
Expand Down
Loading
Loading