diff --git a/cw_evm/lib/usdt0/usdt0_config.dart b/cw_evm/lib/usdt0/usdt0_config.dart index e1978b7a5..292a27757 100644 --- a/cw_evm/lib/usdt0/usdt0_config.dart +++ b/cw_evm/lib/usdt0/usdt0_config.dart @@ -1,7 +1,7 @@ import 'package:cw_core/erc20_token.dart'; /// USDT0 (Omnichain USDT) config. -/// Addresses and EIDs from https://docs.usdt0.to/technical-documentation/developer/usdt0-deployments +/// Addresses and EIDs from https://docs.usdt0.to/technical-documentation/deployments class USDT0Config { USDT0Config._(); diff --git a/integration_test/robots/wallet_keys_robot.dart b/integration_test/robots/wallet_keys_robot.dart index 548874038..6ec9f9df7 100644 --- a/integration_test/robots/wallet_keys_robot.dart +++ b/integration_test/robots/wallet_keys_robot.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/monero_wallet_keys.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; -import 'package:cw_wownero/wownero_wallet.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:polyseed/polyseed.dart'; @@ -55,19 +54,6 @@ class WalletKeysAndSeedPageRobot { ); } - if (walletType == WalletType.wownero) { - final wowneroWallet = appStore.wallet as WowneroWallet; - final lang = PolyseedLang.getByPhrase(wowneroWallet.seed); - final legacySeed = wowneroWallet.seedLegacy(lang.nameEnglish); - - await _confirmMoneroWalletCredentials( - appStore, - walletName, - wowneroWallet.seed, - legacySeed, - ); - } - if (walletType == WalletType.bitcoin || walletType == WalletType.litecoin || walletType == WalletType.bitcoinCash) { diff --git a/lib/di.dart b/lib/di.dart index 7d386e58c..a4b3c0fd1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -209,8 +209,8 @@ import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/backup_view_model.dart'; -import 'package:cake_wallet/view_model/bridge_details_view_model.dart'; -import 'package:cake_wallet/view_model/bridge_history_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_details_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 1462dbe3c..7dce83c7b 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -517,7 +517,7 @@ class CWEVM extends EVM { ChainInfo? getChainInfoByChainId(int chainId) { final config = _registry.getChainConfig(chainId); if (config == null) return null; - + return ChainInfo( chainId: config.chainId, name: config.name, @@ -602,26 +602,30 @@ class CWEVM extends EVM { } @override - Future quoteUSDT0Transfer({ + Future quoteUSDT0Transfer({ required WalletBase wallet, required int sourceChainId, required int destinationChainId, required BigInt amount, required String recipientAddress, - }) { + }) async { final evmWallet = wallet as EVMChainWallet; final client = evmWallet.getWeb3Client(); if (client == null) { throw StateError('Wallet not connected'); } - return USDT0Service.quoteCrossChainTransfer( + final quote = await USDT0Service.quoteCrossChainTransfer( client: client, sourceChainId: sourceChainId, destinationChainId: destinationChainId, amount: amount, recipientAddress: recipientAddress, ); + return BridgeQuote( + nativeFee: quote.nativeFee, + lzTokenFee: quote.lzTokenFee, + ); } @override @@ -632,7 +636,7 @@ class CWEVM extends EVM { required int destinationChainId, required BigInt amount, required String recipientAddress, - required USDT0Quote quote, + required BridgeQuote quote, required TransactionPriority priority, bool useBlinkProtection = true, }) { @@ -645,13 +649,13 @@ class CWEVM extends EVM { destinationChainId: destinationChainId, amount: amount, recipientAddress: recipientAddress, - quote: quote, + quote: USDT0Quote(nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee), token: tokenErc20, priority: priority as EVMChainTransactionPriority, useBlinkProtection: useBlinkProtection, - - ); + ); } + Future getWCBufferedFeeQuote( WalletBase wallet, TransactionPriority priority, diff --git a/lib/new-ui/pages/bridge/bridge_amount_page.dart b/lib/new-ui/pages/bridge/bridge_amount_page.dart index 4e953591d..757a9579e 100644 --- a/lib/new-ui/pages/bridge/bridge_amount_page.dart +++ b/lib/new-ui/pages/bridge/bridge_amount_page.dart @@ -6,7 +6,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.da import 'package:cake_wallet/utils/request_review_handler.dart'; import 'package:cake_wallet/new-ui/widgets/modern_button.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/view_model/bridge_history_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart'; import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; @@ -72,12 +72,12 @@ class _BridgeAmountPageState extends State { doubleActionLeftButtonText: S.of(bottomSheetContext).close, doubleActionRightButtonText: 'View history', onLeftActionButtonPressed: () { - bridgeViewModel.clearBridgeSuccess(); + bridgeViewModel.clearOnBridgeSuccess(); Navigator.of(context, rootNavigator: true).pop(); RequestReviewHandler.requestReview(); }, onRightActionButtonPressed: () { - bridgeViewModel.clearBridgeSuccess(); + bridgeViewModel.clearOnBridgeSuccess(); Navigator.of(context).popUntil((route) => route.isFirst); showMaterialModalBottomSheet( context: context, @@ -96,6 +96,7 @@ class _BridgeAmountPageState extends State { bridgeViewModel.onBridgeSuccess = null; bridgeViewModel.setAmount(''); _amountController.dispose(); + bridgeViewModel.dispose(); super.dispose(); } @@ -165,7 +166,9 @@ class _BridgeAmountPageState extends State { hoverColor: Colors.transparent, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, - hintText: _amountFocused || _amountController.text.isNotEmpty ? null : "0.00", + hintText: _amountFocused || _amountController.text.isNotEmpty + ? null + : "0.00", hintStyle: theme.textTheme.displayMedium?.copyWith( fontWeight: FontWeight.w400, color: theme.colorScheme.onSurfaceVariant, @@ -273,16 +276,16 @@ class _BridgeAmountPageState extends State { icon: Icon(Icons.arrow_forward, size: 20), onPressed: () { Navigator.pushNamed( - context, Routes.bridgeDestinationNetworkPage, - arguments: bridgeViewModel); + context, + Routes.bridgeDestinationNetworkPage, + arguments: bridgeViewModel, + ); }, ), ), ], ), - SizedBox( - height: 24, - ) + SizedBox(height: 24) ], ); }, diff --git a/lib/new-ui/pages/bridge/bridge_detail_page.dart b/lib/new-ui/pages/bridge/bridge_detail_page.dart index a5076ad14..64cc457fd 100644 --- a/lib/new-ui/pages/bridge/bridge_detail_page.dart +++ b/lib/new-ui/pages/bridge/bridge_detail_page.dart @@ -8,7 +8,7 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart'; import 'package:cake_wallet/themes/core/custom_theme_colors.dart'; import 'package:cake_wallet/utils/address_formatter.dart'; import 'package:cake_wallet/utils/show_bar.dart'; -import 'package:cake_wallet/view_model/bridge_details_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_details_view_model.dart'; import 'package:cw_core/generate_name.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/new-ui/pages/bridge/bridge_history_page.dart b/lib/new-ui/pages/bridge/bridge_history_page.dart index a50cc5642..5dff00f31 100644 --- a/lib/new-ui/pages/bridge/bridge_history_page.dart +++ b/lib/new-ui/pages/bridge/bridge_history_page.dart @@ -1,7 +1,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart'; import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/view_model/bridge_history_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart'; import 'package:cake_wallet/new-ui/widgets/bridge/transfer_history_row.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; diff --git a/lib/router.dart b/lib/router.dart index 7af4f85fa..a5af558eb 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -140,7 +140,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart'; -import 'package:cake_wallet/view_model/bridge_history_view_model.dart'; +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart'; diff --git a/lib/view_model/bridge_details_view_model.dart b/lib/view_model/bridge/bridge_details_view_model.dart similarity index 100% rename from lib/view_model/bridge_details_view_model.dart rename to lib/view_model/bridge/bridge_details_view_model.dart diff --git a/lib/view_model/bridge_history_view_model.dart b/lib/view_model/bridge/bridge_history_view_model.dart similarity index 100% rename from lib/view_model/bridge_history_view_model.dart rename to lib/view_model/bridge/bridge_history_view_model.dart diff --git a/lib/view_model/bridge/bridge_view_model.dart b/lib/view_model/bridge/bridge_view_model.dart index 2469fa1b2..b178d8e1c 100644 --- a/lib/view_model/bridge/bridge_view_model.dart +++ b/lib/view_model/bridge/bridge_view_model.dart @@ -44,11 +44,16 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy; void Function()? onBridgeSuccess; - final Map> _pollingCancellers = {}; - final BridgeTransfersStore bridgeTransfersStore; final WalletManager walletManager; - final FiatConversionStore fiatConversionStore; final SettingsStore settingsStore; + final FiatConversionStore fiatConversionStore; + final BridgeTransfersStore bridgeTransfersStore; + final Map> _pollingCancellers = {}; + + static const _pollInterval = Duration(seconds: 2); + static const _pollTimeout = Duration(minutes: 5); + static const _destinationPollInterval = Duration(seconds: 5); + static const _destinationPollTimeout = Duration(minutes: 10); @observable ObservableList bridgeReceivingWalletOptions = @@ -73,7 +78,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St String? destinationWalletName; @observable - USDT0Quote? quote; + BridgeQuote? quote; @observable bool isQuoteLoading = false; @@ -99,6 +104,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St @computed String get sourceAddress => wallet.walletAddresses.address; + @computed + String get fiatCurrencyTitle => settingsStore.fiatCurrency.title; + @computed List get availableDestinationChains { if (!isEVMCompatibleChain(wallet.type)) return []; @@ -122,28 +130,23 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St } @computed - String get tokenBalanceFormatted { - final token = selectedToken; - if (token is! Erc20Token) return '0.00'; - - return amountParsingProxy.getDisplayCryptoStringFromBigInt( - selectedTokenBalance, - token, - ); - } + String get tokenBalanceFormatted { + if (selectedToken == null) return "0.00"; + + return amountParsingProxy.getDisplayCryptoStringFromBigInt( + selectedTokenBalance, selectedToken!, + ); +} @computed String get amountDisplayFormatted { - if (amount.isEmpty) return ''; - final token = selectedToken; - if (token is! Erc20Token) return amount.replaceAll(',', '.'); - - return amountParsingProxy.getDisplayCryptoAmount( - amount.replaceAll(',', '.'), - token, - ); - } - + if (selectedToken == null) return "0.00"; + + return amountParsingProxy.getDisplayCryptoAmount( + amount.replaceAll(',', '.'), + selectedToken!, + ); + } DecimalAmountValidator get decimalAmountValidator => DecimalAmountValidator( currency: selectedToken!, @@ -152,35 +155,28 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St @computed String get fiatAmountFormatted { - if (amount.isEmpty) return ''; - final token = selectedToken; - if (token is! Erc20Token) return ''; + try { + if (amount.isEmpty) return ''; - final price = fiatConversionStore.prices[token]; - if (price == null) return ''; + final price = fiatConversionStore.prices[selectedToken!]; + if (price == null) return ''; - final forFiat = amountParsingProxy.getDisplayCryptoAmount( - amount.replaceAll(',', '.'), - token, - ); + final forFiat = + amountParsingProxy.getDisplayCryptoAmount(amount.replaceAll(',', '.'), selectedToken!); - return calculateFiatAmount( - price: price, - cryptoAmount: forFiat, - ); + return calculateFiatAmount(price: price, cryptoAmount: forFiat); + } catch (_) { + return ''; + } } - @computed - String get fiatCurrencyTitle => settingsStore.fiatCurrency.title; - @computed String get quoteNativeFee { if (quote == null) return '—'; - final cur = wallet.currency; return amountParsingProxy.getDisplayCryptoStringFromBigInt( quote!.nativeFee, - cur, + wallet.currency, ); } @@ -193,70 +189,93 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St @computed String get quoteNativeFiatFeeFormattedForDisplay { - if (quote == null || quoteNativeFee.isEmpty) return ''; + try { + if (quote == null || quoteNativeFee.isEmpty) return ''; - final price = fiatConversionStore.prices[wallet.currency]; - if (price == null) return ''; + final price = fiatConversionStore.prices[wallet.currency]; + if (price == null) return ''; - final fiatFeeFormatted = calculateFiatAmount( - price: price, - cryptoAmount: amountParsingProxy.getDisplayCryptoAmount( - quoteNativeFee.replaceAll(',', '.'), - wallet.currency, - ), - ); + final fiatFeeFormatted = calculateFiatAmount( + price: price, + cryptoAmount: amountParsingProxy.getDisplayCryptoAmount( + quoteNativeFee.replaceAll(',', '.'), + wallet.currency, + ), + ); - return '(${settingsStore.fiatCurrency.title} $fiatFeeFormatted)'; + return '(${fiatCurrencyTitle} $fiatFeeFormatted)'; + } catch (_) { + return ''; + } } @computed bool get canProceedToDestinationNetwork { if (amount.isEmpty) return false; - if (selectedToken == null || selectedToken is! Erc20Token) return false; + if (selectedToken.isNotErc20) return false; if (amountError != null) return false; - final token = selectedToken as Erc20Token; final validAmount = amountParsingProxy.tryParseCryptoString( amount.replaceAll(',', '.'), - token, + selectedToken!, ); return validAmount != null && validAmount > BigInt.zero; } + @computed + BigInt get selectedTokenBalance { + if (selectedToken == null) return BigInt.zero; + + try { + final bal = wallet.balance[selectedToken!]; + + return bal?.fullAvailableBalance ?? BigInt.zero; + } catch (e) { + return BigInt.zero; + } + } + + @computed + String? get amountError { + if (selectedToken == null) return null; + + final amountBigInt = amountParsingProxy.tryParseCryptoString( + amount.replaceAll(',', '.'), + selectedToken!, + ); + + if (amountBigInt == null || amountBigInt == BigInt.zero) return null; + if (amountBigInt > selectedTokenBalance) { + return 'Insufficient balance for ${selectedToken!.title} token.'; + } + + return null; + } + @action void applyInitialBridgeToken(CryptoCurrency asset) { - for (final t in availableUSDT0Tokens) { - if (t == asset) { - setSelectedToken(t); - break; - } + final token = availableUSDT0Tokens.firstWhereOrNull((t) => t == asset); + if (token != null) { + setSelectedToken(token); } } @action - void setDestinationChain(int chainId) { - destinationChainId = chainId; - _clearQuoteState(); - } + void setDestinationChain(int chainId) => destinationChainId = chainId; @action - void setSelectedToken(CryptoCurrency token) { - selectedToken = token; - _clearQuoteState(); - } + void setSelectedToken(CryptoCurrency token) => selectedToken = token; @action - void setAmount(String value) { - amount = value; - _clearQuoteState(); - } + void setAmount(String value) => amount = value; @action void setMaxAmount() { - final token = selectedToken; - if (token is! Erc20Token) return; + final token = selectedToken.asErc20; + if (token == null) return; + if (selectedTokenBalance == BigInt.zero) { setAmount(''); return; @@ -273,27 +292,24 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St void setRecipientAddress(String value, {String? destWalletName}) { recipientAddress = value; destinationWalletName = destWalletName; - _clearQuoteState(); - } - - void _clearQuoteState() { - quote = null; - quoteError = null; - executeError = null; } Future _ensureFiatPriceFor(CryptoCurrency crypto) async { if (fiatConversionStore.prices[crypto] != null) return; - final p = await FiatConversionService.fetchPrice( - crypto: crypto, - fiat: settingsStore.fiatCurrency, - torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly, - ); + try { + final p = await FiatConversionService.fetchPrice( + crypto: crypto, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly, + ); - runInAction(() { - fiatConversionStore.prices[crypto] = p; - }); + runInAction(() { + fiatConversionStore.prices[crypto] = p; + }); + } catch (e) { + printV('Error ensuring fiat price for $crypto: $e'); + } } @action @@ -306,35 +322,12 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St await _ensureFiatPriceFor(wallet.currency); } - @computed - BigInt get selectedTokenBalance { - final bal = wallet.balance[selectedToken]; - - return bal?.available ?? BigInt.zero; - } - - @computed - String? get amountError { - if (selectedToken == null || amount.isEmpty) return null; - if (selectedToken is! Erc20Token) return null; - - final token = selectedToken as Erc20Token; - final amountBigInt = amountParsingProxy.tryParseCryptoString( - amount.replaceAll(',', '.'), - token, - ); - if (amountBigInt == null || amountBigInt == BigInt.zero) return null; - if (amountBigInt > selectedTokenBalance) { - return 'Insufficient balance for ${token.title} token.'; - } - - return null; - } - @action Future loadReceivingWalletOptions() async { if (!isEVMCompatibleChain(wallet.type)) return; + if (destinationChainId == null) return; + final destWalletType = evm!.getWalletTypeByChainId(destinationChainId!); isBridgeReceivingWalletListLoading = true; @@ -366,6 +359,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St bridgeReceivingWalletOptions ..clear() ..addAll(options); + } catch (e) { + printV('Error loading receiving wallet options: $e'); } finally { isBridgeReceivingWalletListLoading = false; } @@ -414,8 +409,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St return; } - final token = selectedToken!; - if (token is! Erc20Token) return; + final token = selectedToken.asErc20; + if (token == null) return; final check = _parseAndValidateAmount(token); if (check.error != null) { @@ -428,9 +423,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St final amountBigInt = check.parsedAmount!; isQuoteLoading = true; - quoteError = null; - quote = null; - executeError = null; + _clearQuoteState(); try { quote = await evm!.quoteUSDT0Transfer( @@ -460,8 +453,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St return; } - final token = selectedToken!; - if (token is! Erc20Token) return; + final token = selectedToken.asErc20; + if (token == null) return; final check = _parseAndValidateAmount(token); if (check.error != null) { @@ -513,7 +506,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St lastCreatedBridgeTransfer = record; }); onBridgeSuccess?.call(); - _pollForSourceConfirmation(record, wallet); + _pollForConfirmation(record, wallet, isSource: true); } catch (e) { executeError = e.toString(); } finally { @@ -521,26 +514,12 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St } } - static const _pollInterval = Duration(seconds: 2); - static const _pollTimeout = Duration(minutes: 3); - static const _destinationPollInterval = Duration(seconds: 5); - static const _destinationPollTimeout = Duration(minutes: 10); - @override void onWalletChange(WalletBase wallet) { _cancelAllPolling(); _resumePollingForActiveTransfers(wallet); } - void _cancelAllPolling() { - for (final canceller in _pollingCancellers.values) { - if (!canceller.isCompleted) { - canceller.complete(); - } - } - _pollingCancellers.clear(); - } - void _resumePollingForActiveTransfers(WalletBase wallet) { if (!isEVMCompatibleChain(wallet.type)) return; @@ -550,9 +529,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St for (final transfer in activeTransfers) { if (transfer.status == 'submitted' || transfer.status == 'confirming') { - _pollForSourceConfirmation(transfer, wallet); + _pollForConfirmation(transfer, wallet, isSource: true); } else if (transfer.status == 'initiated') { - _pollForDestinationCompletion(transfer, wallet); + _pollForConfirmation(transfer, wallet, isSource: false); } } } @@ -579,130 +558,134 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St if (statusMessage != null) record.statusMessage = statusMessage; if (confirmedAt != null) record.confirmedAt = confirmedAt; }); - await bridgeTransfersStore.updateTransfer(record); - } - - Future _pollForSourceConfirmation( - BridgeTransfer record, - WalletBase wallet, - ) async { - final canceller = Completer(); - _pollingCancellers[record.id] = canceller; - final walletId = wallet.name; - final deadline = DateTime.now().add(_pollTimeout); try { - while (DateTime.now().isBefore(deadline)) { - await Future.any([ - Future.delayed(_pollInterval), - canceller.future, - ]); - - if (canceller.isCompleted || !_isValidWalletContext(walletId)) return; - - bool? receipt; - try { - receipt = await evm!.getTransactionReceipt(wallet, record.sourceTxHash); - } catch (e) { - printV('USDT0 bridge: Error fetching receipt: $e'); - continue; - } - - if (receipt == null) continue; - - if (receipt == true) { - await _updateTransferStatus( - record, - 'confirming', - confirmedAt: DateTime.now(), - ); - - await Future.delayed(const Duration(seconds: 1)); - if (canceller.isCompleted || !_isValidWalletContext(walletId)) return; - - await _updateTransferStatus(record, 'initiated'); - _pollForDestinationCompletion(record, wallet); - - return; - } else if (receipt == false) { - await _updateTransferStatus( - record, - 'failed', - errorMessage: 'Transaction reverted', - ); - return; - } - } - - if (!_isValidWalletContext(walletId)) return; - - await _updateTransferStatus(record, 'initiated'); - _pollForDestinationCompletion(record, wallet); - } finally { - _pollingCancellers.remove(record.id); + await bridgeTransfersStore.updateTransfer(record); + } catch (e) { + printV('USDT0 bridge: Error updating transfer status: $e'); } } - Future _pollForDestinationCompletion( + Future _pollForConfirmation( BridgeTransfer record, - WalletBase wallet, - ) async { + WalletBase wallet, { + required bool isSource, + }) async { final canceller = Completer(); - _pollingCancellers['${record.id}_dest'] = canceller; + final recordId = isSource ? record.id : '${record.id}_dest'; + final pollInterval = isSource ? _pollInterval : _destinationPollInterval; + final pollTimeout = isSource ? _pollTimeout : _destinationPollTimeout; final walletId = wallet.name; - final deadline = DateTime.now().add(_destinationPollTimeout); + final deadline = DateTime.now().add(pollTimeout); + + _pollingCancellers[recordId] = canceller; try { while (DateTime.now().isBefore(deadline)) { await Future.any([ - Future.delayed(_destinationPollInterval), + Future.delayed(pollInterval), canceller.future, ]); if (canceller.isCompleted || !_isValidWalletContext(walletId)) return; - LayerZeroMessageStatus? status; - try { - status = await LayerZeroScanService.getMessageStatus(record.sourceTxHash); - } catch (e) { - printV('USDT0 bridge: Error fetching LayerZero status: $e'); + if (isSource) { + final receipt = await _fetchTransactionReceipt(record, wallet); + + if (receipt != null) { + final isTransactionSuccessful = receipt == true; + await _updateTransferStatus( + record, + isTransactionSuccessful ? 'initiated' : 'failed', + confirmedAt: isTransactionSuccessful ? DateTime.now() : null, + errorMessage: !isTransactionSuccessful ? 'Transaction reverted' : null, + ); + + if (isTransactionSuccessful) { + await Future.delayed(const Duration(seconds: 1)); + if (_isValidWalletContext(walletId)) { + _pollForConfirmation(record, wallet, isSource: false); + } + } + + return; + } + + continue; + } else { + final status = await _fetchLayerZeroMessageStatus(record, wallet); + + if (status != null) { + final statusMessage = _getStatusMessage(status, record); + await _updateTransferStatus( + record, + statusMessage, + errorMessage: + status.isFailed ? status.status?.message ?? 'Bridge message failed' : null, + statusMessage: status.status?.message, + ); + + if (status.isDelivered || status.isFailed) return; + } + continue; } + } - if (status == null) continue; - - if (status.isDelivered) { - await _updateTransferStatus( - record, - 'completed', - statusMessage: status.status?.message, - ); - return; - } - - if (status.isFailed) { - await _updateTransferStatus( - record, - 'failed', - errorMessage: status.status?.message ?? 'Bridge message failed', - statusMessage: status.status?.message, - ); - return; - } - + if (isSource && _isValidWalletContext(walletId)) { await _updateTransferStatus( record, - record.status, - statusMessage: status.status?.message, + 'failed', + errorMessage: 'Source confirmation timed out', ); } + } catch (e) { + printV('USDT0 bridge: Error polling for confirmation: $e'); } finally { - _pollingCancellers.remove('${record.id}_dest'); + _pollingCancellers.remove(recordId); + } + } + + String _getStatusMessage(LayerZeroMessageStatus status, BridgeTransfer record) { + if (status.isDelivered) { + return 'completed'; + } + + if (status.isFailed) { + return 'failed'; + } + + return record.status; + } + + Future _fetchTransactionReceipt(BridgeTransfer record, WalletBase wallet) async { + try { + return await evm!.getTransactionReceipt(wallet, record.sourceTxHash); + } catch (e) { + printV('USDT0 bridge: Error fetching receipt: $e'); + return null; + } + } + + Future _fetchLayerZeroMessageStatus( + BridgeTransfer record, WalletBase wallet) async { + try { + return await LayerZeroScanService.getMessageStatus(record.sourceTxHash); + } catch (e) { + printV('USDT0 bridge: Error fetching LayerZero status: $e'); + return null; } } @action - void clearBridgeSuccess() { + void _clearQuoteState() { + quote = null; + quoteError = null; + executeError = null; + } + + @action + void clearOnBridgeSuccess() { amount = ''; recipientAddress = ''; destinationWalletName = null; @@ -712,7 +695,25 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St _clearQuoteState(); } + void _cancelAllPolling() { + for (final canceller in _pollingCancellers.values) { + if (!canceller.isCompleted) { + canceller.complete(); + } + } + _pollingCancellers.clear(); + } + void dispose() { _cancelAllPolling(); } } + +extension CryptoCurrencyX on CryptoCurrency? { + Erc20Token? get asErc20 { + final token = this; + return token is Erc20Token ? token : null; + } + + bool get isNotErc20 => this == null || this is! Erc20Token; +} diff --git a/tool/configure.dart b/tool/configure.dart index afbdae4f2..507b69007 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1570,7 +1570,7 @@ abstract class EVM { bool isUSDT0Token(WalletBase wallet, CryptoCurrency token); List getUSDT0DestinationChains(WalletBase wallet); - Future quoteUSDT0Transfer({ + Future quoteUSDT0Transfer({ required WalletBase wallet, required int sourceChainId, required int destinationChainId, @@ -1585,7 +1585,7 @@ abstract class EVM { required int destinationChainId, required BigInt amount, required String recipientAddress, - required USDT0Quote quote, + required BridgeQuote quote, required TransactionPriority priority, bool useBlinkProtection = true, }); @@ -1631,6 +1631,16 @@ class EvmWalletConnectFeeQuote { final int maxPriorityFeePerGasWei; final int? latestBaseFeeWei; } + +class BridgeQuote { + const BridgeQuote({ + required this.nativeFee, + required this.lzTokenFee, + }); + + final BigInt nativeFee; + final BigInt lzTokenFee; +} """; const evmEmptyDefinition = 'EVM? evm;\n';