refactor: Improve bridge view model (#3184)

* refactor: Improve bridge view model * Move bridge related vms into single folder * handle missing class when building for monero.com * handle missing classes in monero.com build * Minor enhancements * Update lib/view_model/bridge/bridge_view_model.dart [skip ci] * Update lib/view_model/bridge/bridge_view_model.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Apr 30, 2026 at 19:15 UTC 40c4118b660fd6992dc4aac43939a639ad9794f8
12 files changed +274 -270
cw_evm/lib/usdt0/usdt0_config.dart
+1 -1
@@ -1,7 +1,7 @@
1 import 'package:cw_core/erc20_token.dart';
2
3 /// USDT0 (Omnichain USDT) config.
4 -/// Addresses and EIDs from https://docs.usdt0.to/technical-documentation/developer/usdt0-deployments
4 +/// Addresses and EIDs from https://docs.usdt0.to/technical-documentation/deployments
5 class USDT0Config {
6 USDT0Config._();
7
integration_test/robots/wallet_keys_robot.dart
-14
@@ -5,7 +5,6 @@ import 'package:cake_wallet/store/app_store.dart';
5 import 'package:cw_core/monero_wallet_keys.dart';
6 import 'package:cw_core/wallet_type.dart';
7 import 'package:cw_monero/monero_wallet.dart';
8 -import 'package:cw_wownero/wownero_wallet.dart';
8 import 'package:flutter_test/flutter_test.dart';
9 import 'package:polyseed/polyseed.dart';
10
@@ -55,19 +54,6 @@ class WalletKeysAndSeedPageRobot {
54 );
55 }
56
58 - if (walletType == WalletType.wownero) {
59 - final wowneroWallet = appStore.wallet as WowneroWallet;
60 - final lang = PolyseedLang.getByPhrase(wowneroWallet.seed);
61 - final legacySeed = wowneroWallet.seedLegacy(lang.nameEnglish);
62 -
63 - await _confirmMoneroWalletCredentials(
64 - appStore,
65 - walletName,
66 - wowneroWallet.seed,
67 - legacySeed,
68 - );
69 - }
70 -
57 if (walletType == WalletType.bitcoin ||
58 walletType == WalletType.litecoin ||
59 walletType == WalletType.bitcoinCash) {
lib/di.dart
+2 -2
@@ -209,8 +209,8 @@ import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
209 import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
210 import 'package:cake_wallet/view_model/auth_view_model.dart';
211 import 'package:cake_wallet/view_model/backup_view_model.dart';
212 -import 'package:cake_wallet/view_model/bridge_details_view_model.dart';
213 -import 'package:cake_wallet/view_model/bridge_history_view_model.dart';
212 +import 'package:cake_wallet/view_model/bridge/bridge_details_view_model.dart';
213 +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
214 import 'package:cake_wallet/view_model/buy/buy_amount_view_model.dart';
215 import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
216 import 'package:cake_wallet/view_model/buy/buy_view_model.dart';
lib/evm/cw_evm.dart
+12 -8
@@ -517,7 +517,7 @@ class CWEVM extends EVM {
517 ChainInfo? getChainInfoByChainId(int chainId) {
518 final config = _registry.getChainConfig(chainId);
519 if (config == null) return null;
520 -
520 +
521 return ChainInfo(
522 chainId: config.chainId,
523 name: config.name,
@@ -602,26 +602,30 @@ class CWEVM extends EVM {
602 }
603
604 @override
605 - Future<USDT0Quote> quoteUSDT0Transfer({
605 + Future<BridgeQuote> quoteUSDT0Transfer({
606 required WalletBase wallet,
607 required int sourceChainId,
608 required int destinationChainId,
609 required BigInt amount,
610 required String recipientAddress,
611 - }) {
611 + }) async {
612 final evmWallet = wallet as EVMChainWallet;
613 final client = evmWallet.getWeb3Client();
614 if (client == null) {
615 throw StateError('Wallet not connected');
616 }
617
618 - return USDT0Service.quoteCrossChainTransfer(
618 + final quote = await USDT0Service.quoteCrossChainTransfer(
619 client: client,
620 sourceChainId: sourceChainId,
621 destinationChainId: destinationChainId,
622 amount: amount,
623 recipientAddress: recipientAddress,
624 );
625 + return BridgeQuote(
626 + nativeFee: quote.nativeFee,
627 + lzTokenFee: quote.lzTokenFee,
628 + );
629 }
630
631 @override
@@ -632,7 +636,7 @@ class CWEVM extends EVM {
636 required int destinationChainId,
637 required BigInt amount,
638 required String recipientAddress,
635 - required USDT0Quote quote,
639 + required BridgeQuote quote,
640 required TransactionPriority priority,
641 bool useBlinkProtection = true,
642 }) {
@@ -645,13 +649,13 @@ class CWEVM extends EVM {
649 destinationChainId: destinationChainId,
650 amount: amount,
651 recipientAddress: recipientAddress,
648 - quote: quote,
652 + quote: USDT0Quote(nativeFee: quote.nativeFee, lzTokenFee: quote.lzTokenFee),
653 token: tokenErc20,
654 priority: priority as EVMChainTransactionPriority,
655 useBlinkProtection: useBlinkProtection,
652 -
653 - );
656 + );
657 }
658 +
659 Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
660 WalletBase wallet,
661 TransactionPriority priority,
lib/new-ui/pages/bridge/bridge_amount_page.dart
+12 -9
@@ -6,7 +6,7 @@ import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.da
6 import 'package:cake_wallet/utils/request_review_handler.dart';
7 import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
8 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
9 -import 'package:cake_wallet/view_model/bridge_history_view_model.dart';
9 +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
10 import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
11 import 'package:cw_core/crypto_currency.dart';
12 import 'package:flutter/material.dart';
@@ -72,12 +72,12 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
72 doubleActionLeftButtonText: S.of(bottomSheetContext).close,
73 doubleActionRightButtonText: 'View history',
74 onLeftActionButtonPressed: () {
75 - bridgeViewModel.clearBridgeSuccess();
75 + bridgeViewModel.clearOnBridgeSuccess();
76 Navigator.of(context, rootNavigator: true).pop();
77 RequestReviewHandler.requestReview();
78 },
79 onRightActionButtonPressed: () {
80 - bridgeViewModel.clearBridgeSuccess();
80 + bridgeViewModel.clearOnBridgeSuccess();
81 Navigator.of(context).popUntil((route) => route.isFirst);
82 showMaterialModalBottomSheet(
83 context: context,
@@ -96,6 +96,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
96 bridgeViewModel.onBridgeSuccess = null;
97 bridgeViewModel.setAmount('');
98 _amountController.dispose();
99 + bridgeViewModel.dispose();
100 super.dispose();
101 }
102
@@ -165,7 +166,9 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
166 hoverColor: Colors.transparent,
167 focusedBorder: InputBorder.none,
168 enabledBorder: InputBorder.none,
168 - hintText: _amountFocused || _amountController.text.isNotEmpty ? null : "0.00",
169 + hintText: _amountFocused || _amountController.text.isNotEmpty
170 + ? null
171 + : "0.00",
172 hintStyle: theme.textTheme.displayMedium?.copyWith(
173 fontWeight: FontWeight.w400,
174 color: theme.colorScheme.onSurfaceVariant,
@@ -273,16 +276,16 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
276 icon: Icon(Icons.arrow_forward, size: 20),
277 onPressed: () {
278 Navigator.pushNamed(
276 - context, Routes.bridgeDestinationNetworkPage,
277 - arguments: bridgeViewModel);
279 + context,
280 + Routes.bridgeDestinationNetworkPage,
281 + arguments: bridgeViewModel,
282 + );
283 },
284 ),
285 ),
286 ],
287 ),
283 - SizedBox(
284 - height: 24,
285 - )
288 + SizedBox(height: 24)
289 ],
290 );
291 },
lib/new-ui/pages/bridge/bridge_detail_page.dart
+1 -1
@@ -8,7 +8,7 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
8 import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
9 import 'package:cake_wallet/utils/address_formatter.dart';
10 import 'package:cake_wallet/utils/show_bar.dart';
11 -import 'package:cake_wallet/view_model/bridge_details_view_model.dart';
11 +import 'package:cake_wallet/view_model/bridge/bridge_details_view_model.dart';
12 import 'package:cw_core/generate_name.dart';
13 import 'package:flutter/material.dart';
14 import 'package:flutter/services.dart';
lib/new-ui/pages/bridge/bridge_history_page.dart
+1 -1
@@ -1,7 +1,7 @@
1 import 'package:cake_wallet/di.dart';
2 import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
3 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
4 -import 'package:cake_wallet/view_model/bridge_history_view_model.dart';
4 +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
5 import 'package:cake_wallet/new-ui/widgets/bridge/transfer_history_row.dart';
6 import 'package:flutter/material.dart';
7 import 'package:flutter_mobx/flutter_mobx.dart';
lib/router.dart
+1 -1
@@ -140,7 +140,7 @@ import 'package:cake_wallet/store/settings_store.dart';
140 import 'package:cake_wallet/utils/payment_request.dart';
141 import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
142 import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
143 -import 'package:cake_wallet/view_model/bridge_history_view_model.dart';
143 +import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
144 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
145 import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
146 import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
lib/view_model/bridge/bridge_details_view_model.dart renamed
lib/view_model/bridge/bridge_history_view_model.dart renamed
lib/view_model/bridge/bridge_view_model.dart
+232 -231
@@ -44,11 +44,16 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
44 AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
45
46 void Function()? onBridgeSuccess;
47 - final Map<String, Completer<void>> _pollingCancellers = {};
48 - final BridgeTransfersStore bridgeTransfersStore;
47 final WalletManager walletManager;
50 - final FiatConversionStore fiatConversionStore;
48 final SettingsStore settingsStore;
49 + final FiatConversionStore fiatConversionStore;
50 + final BridgeTransfersStore bridgeTransfersStore;
51 + final Map<String, Completer<void>> _pollingCancellers = {};
52 +
53 + static const _pollInterval = Duration(seconds: 2);
54 + static const _pollTimeout = Duration(minutes: 5);
55 + static const _destinationPollInterval = Duration(seconds: 5);
56 + static const _destinationPollTimeout = Duration(minutes: 10);
57
58 @observable
59 ObservableList<BridgeReceivingWalletOption> bridgeReceivingWalletOptions =
@@ -73,7 +78,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
78 String? destinationWalletName;
79
80 @observable
76 - USDT0Quote? quote;
81 + BridgeQuote? quote;
82
83 @observable
84 bool isQuoteLoading = false;
@@ -99,6 +104,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
104 @computed
105 String get sourceAddress => wallet.walletAddresses.address;
106
107 + @computed
108 + String get fiatCurrencyTitle => settingsStore.fiatCurrency.title;
109 +
110 @computed
111 List<ChainInfo> get availableDestinationChains {
112 if (!isEVMCompatibleChain(wallet.type)) return [];
@@ -122,28 +130,23 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
130 }
131
132 @computed
125 - String get tokenBalanceFormatted {
126 - final token = selectedToken;
127 - if (token is! Erc20Token) return '0.00';
128 -
129 - return amountParsingProxy.getDisplayCryptoStringFromBigInt(
130 - selectedTokenBalance,
131 - token,
132 - );
133 - }
133 + String get tokenBalanceFormatted {
134 + if (selectedToken == null) return "0.00";
135 +
136 + return amountParsingProxy.getDisplayCryptoStringFromBigInt(
137 + selectedTokenBalance, selectedToken!,
138 + );
139 +}
140
141 @computed
142 String get amountDisplayFormatted {
137 - if (amount.isEmpty) return '';
138 - final token = selectedToken;
139 - if (token is! Erc20Token) return amount.replaceAll(',', '.');
140 -
141 - return amountParsingProxy.getDisplayCryptoAmount(
142 - amount.replaceAll(',', '.'),
143 - token,
144 - );
145 - }
146 -
143 + if (selectedToken == null) return "0.00";
144 +
145 + return amountParsingProxy.getDisplayCryptoAmount(
146 + amount.replaceAll(',', '.'),
147 + selectedToken!,
148 + );
149 + }
150
151 DecimalAmountValidator get decimalAmountValidator => DecimalAmountValidator(
152 currency: selectedToken!,
@@ -152,35 +155,28 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
155
156 @computed
157 String get fiatAmountFormatted {
155 - if (amount.isEmpty) return '';
156 - final token = selectedToken;
157 - if (token is! Erc20Token) return '';
158 + try {
159 + if (amount.isEmpty) return '';
160
159 - final price = fiatConversionStore.prices[token];
160 - if (price == null) return '';
161 + final price = fiatConversionStore.prices[selectedToken!];
162 + if (price == null) return '';
163
162 - final forFiat = amountParsingProxy.getDisplayCryptoAmount(
163 - amount.replaceAll(',', '.'),
164 - token,
165 - );
164 + final forFiat =
165 + amountParsingProxy.getDisplayCryptoAmount(amount.replaceAll(',', '.'), selectedToken!);
166
167 - return calculateFiatAmount(
168 - price: price,
169 - cryptoAmount: forFiat,
170 - );
167 + return calculateFiatAmount(price: price, cryptoAmount: forFiat);
168 + } catch (_) {
169 + return '';
170 + }
171 }
172
173 - @computed
174 - String get fiatCurrencyTitle => settingsStore.fiatCurrency.title;
175 -
173 @computed
174 String get quoteNativeFee {
175 if (quote == null) return '—';
176
180 - final cur = wallet.currency;
177 return amountParsingProxy.getDisplayCryptoStringFromBigInt(
178 quote!.nativeFee,
183 - cur,
179 + wallet.currency,
180 );
181 }
182
@@ -193,70 +189,93 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
189
190 @computed
191 String get quoteNativeFiatFeeFormattedForDisplay {
196 - if (quote == null || quoteNativeFee.isEmpty) return '';
192 + try {
193 + if (quote == null || quoteNativeFee.isEmpty) return '';
194
198 - final price = fiatConversionStore.prices[wallet.currency];
199 - if (price == null) return '';
195 + final price = fiatConversionStore.prices[wallet.currency];
196 + if (price == null) return '';
197
201 - final fiatFeeFormatted = calculateFiatAmount(
202 - price: price,
203 - cryptoAmount: amountParsingProxy.getDisplayCryptoAmount(
204 - quoteNativeFee.replaceAll(',', '.'),
205 - wallet.currency,
206 - ),
207 - );
198 + final fiatFeeFormatted = calculateFiatAmount(
199 + price: price,
200 + cryptoAmount: amountParsingProxy.getDisplayCryptoAmount(
201 + quoteNativeFee.replaceAll(',', '.'),
202 + wallet.currency,
203 + ),
204 + );
205
209 - return '(${settingsStore.fiatCurrency.title} $fiatFeeFormatted)';
206 + return '(${fiatCurrencyTitle} $fiatFeeFormatted)';
207 + } catch (_) {
208 + return '';
209 + }
210 }
211
212 @computed
213 bool get canProceedToDestinationNetwork {
214 if (amount.isEmpty) return false;
215
216 - if (selectedToken == null || selectedToken is! Erc20Token) return false;
216 + if (selectedToken.isNotErc20) return false;
217
218 if (amountError != null) return false;
219
220 - final token = selectedToken as Erc20Token;
220 final validAmount = amountParsingProxy.tryParseCryptoString(
221 amount.replaceAll(',', '.'),
223 - token,
222 + selectedToken!,
223 );
224 return validAmount != null && validAmount > BigInt.zero;
225 }
226
227 + @computed
228 + BigInt get selectedTokenBalance {
229 + if (selectedToken == null) return BigInt.zero;
230 +
231 + try {
232 + final bal = wallet.balance[selectedToken!];
233 +
234 + return bal?.fullAvailableBalance ?? BigInt.zero;
235 + } catch (e) {
236 + return BigInt.zero;
237 + }
238 + }
239 +
240 + @computed
241 + String? get amountError {
242 + if (selectedToken == null) return null;
243 +
244 + final amountBigInt = amountParsingProxy.tryParseCryptoString(
245 + amount.replaceAll(',', '.'),
246 + selectedToken!,
247 + );
248 +
249 + if (amountBigInt == null || amountBigInt == BigInt.zero) return null;
250 + if (amountBigInt > selectedTokenBalance) {
251 + return 'Insufficient balance for ${selectedToken!.title} token.';
252 + }
253 +
254 + return null;
255 + }
256 +
257 @action
258 void applyInitialBridgeToken(CryptoCurrency asset) {
230 - for (final t in availableUSDT0Tokens) {
231 - if (t == asset) {
232 - setSelectedToken(t);
233 - break;
234 - }
259 + final token = availableUSDT0Tokens.firstWhereOrNull((t) => t == asset);
260 + if (token != null) {
261 + setSelectedToken(token);
262 }
263 }
264
265 @action
239 - void setDestinationChain(int chainId) {
240 - destinationChainId = chainId;
241 - _clearQuoteState();
242 - }
266 + void setDestinationChain(int chainId) => destinationChainId = chainId;
267
268 @action
245 - void setSelectedToken(CryptoCurrency token) {
246 - selectedToken = token;
247 - _clearQuoteState();
248 - }
269 + void setSelectedToken(CryptoCurrency token) => selectedToken = token;
270
271 @action
251 - void setAmount(String value) {
252 - amount = value;
253 - _clearQuoteState();
254 - }
272 + void setAmount(String value) => amount = value;
273
274 @action
275 void setMaxAmount() {
258 - final token = selectedToken;
259 - if (token is! Erc20Token) return;
276 + final token = selectedToken.asErc20;
277 + if (token == null) return;
278 +
279 if (selectedTokenBalance == BigInt.zero) {
280 setAmount('');
281 return;
@@ -273,27 +292,24 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
292 void setRecipientAddress(String value, {String? destWalletName}) {
293 recipientAddress = value;
294 destinationWalletName = destWalletName;
276 - _clearQuoteState();
277 - }
278 -
279 - void _clearQuoteState() {
280 - quote = null;
281 - quoteError = null;
282 - executeError = null;
295 }
296
297 Future<void> _ensureFiatPriceFor(CryptoCurrency crypto) async {
298 if (fiatConversionStore.prices[crypto] != null) return;
299
288 - final p = await FiatConversionService.fetchPrice(
289 - crypto: crypto,
290 - fiat: settingsStore.fiatCurrency,
291 - torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly,
292 - );
300 + try {
301 + final p = await FiatConversionService.fetchPrice(
302 + crypto: crypto,
303 + fiat: settingsStore.fiatCurrency,
304 + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly,
305 + );
306
294 - runInAction(() {
295 - fiatConversionStore.prices[crypto] = p;
296 - });
307 + runInAction(() {
308 + fiatConversionStore.prices[crypto] = p;
309 + });
310 + } catch (e) {
311 + printV('Error ensuring fiat price for $crypto: $e');
312 + }
313 }
314
315 @action
@@ -306,35 +322,12 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
322 await _ensureFiatPriceFor(wallet.currency);
323 }
324
309 - @computed
310 - BigInt get selectedTokenBalance {
311 - final bal = wallet.balance[selectedToken];
312 -
313 - return bal?.available ?? BigInt.zero;
314 - }
315 -
316 - @computed
317 - String? get amountError {
318 - if (selectedToken == null || amount.isEmpty) return null;
319 - if (selectedToken is! Erc20Token) return null;
320 -
321 - final token = selectedToken as Erc20Token;
322 - final amountBigInt = amountParsingProxy.tryParseCryptoString(
323 - amount.replaceAll(',', '.'),
324 - token,
325 - );
326 - if (amountBigInt == null || amountBigInt == BigInt.zero) return null;
327 - if (amountBigInt > selectedTokenBalance) {
328 - return 'Insufficient balance for ${token.title} token.';
329 - }
330 -
331 - return null;
332 - }
333 -
325 @action
326 Future<void> loadReceivingWalletOptions() async {
327 if (!isEVMCompatibleChain(wallet.type)) return;
328
329 + if (destinationChainId == null) return;
330 +
331 final destWalletType = evm!.getWalletTypeByChainId(destinationChainId!);
332
333 isBridgeReceivingWalletListLoading = true;
@@ -366,6 +359,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
359 bridgeReceivingWalletOptions
360 ..clear()
361 ..addAll(options);
362 + } catch (e) {
363 + printV('Error loading receiving wallet options: $e');
364 } finally {
365 isBridgeReceivingWalletListLoading = false;
366 }
@@ -414,8 +409,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
409 return;
410 }
411
417 - final token = selectedToken!;
418 - if (token is! Erc20Token) return;
412 + final token = selectedToken.asErc20;
413 + if (token == null) return;
414
415 final check = _parseAndValidateAmount(token);
416 if (check.error != null) {
@@ -428,9 +423,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
423 final amountBigInt = check.parsedAmount!;
424
425 isQuoteLoading = true;
431 - quoteError = null;
432 - quote = null;
433 - executeError = null;
426 + _clearQuoteState();
427
428 try {
429 quote = await evm!.quoteUSDT0Transfer(
@@ -460,8 +453,8 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
453 return;
454 }
455
463 - final token = selectedToken!;
464 - if (token is! Erc20Token) return;
456 + final token = selectedToken.asErc20;
457 + if (token == null) return;
458
459 final check = _parseAndValidateAmount(token);
460 if (check.error != null) {
@@ -513,7 +506,7 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
506 lastCreatedBridgeTransfer = record;
507 });
508 onBridgeSuccess?.call();
516 - _pollForSourceConfirmation(record, wallet);
509 + _pollForConfirmation(record, wallet, isSource: true);
510 } catch (e) {
511 executeError = e.toString();
512 } finally {
@@ -521,26 +514,12 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
514 }
515 }
516
524 - static const _pollInterval = Duration(seconds: 2);
525 - static const _pollTimeout = Duration(minutes: 3);
526 - static const _destinationPollInterval = Duration(seconds: 5);
527 - static const _destinationPollTimeout = Duration(minutes: 10);
528 -
517 @override
518 void onWalletChange(WalletBase wallet) {
519 _cancelAllPolling();
520 _resumePollingForActiveTransfers(wallet);
521 }
522
535 - void _cancelAllPolling() {
536 - for (final canceller in _pollingCancellers.values) {
537 - if (!canceller.isCompleted) {
538 - canceller.complete();
539 - }
540 - }
541 - _pollingCancellers.clear();
542 - }
543 -
523 void _resumePollingForActiveTransfers(WalletBase wallet) {
524 if (!isEVMCompatibleChain(wallet.type)) return;
525
@@ -550,9 +529,9 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
529
530 for (final transfer in activeTransfers) {
531 if (transfer.status == 'submitted' || transfer.status == 'confirming') {
553 - _pollForSourceConfirmation(transfer, wallet);
532 + _pollForConfirmation(transfer, wallet, isSource: true);
533 } else if (transfer.status == 'initiated') {
555 - _pollForDestinationCompletion(transfer, wallet);
534 + _pollForConfirmation(transfer, wallet, isSource: false);
535 }
536 }
537 }
@@ -579,130 +558,134 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
558 if (statusMessage != null) record.statusMessage = statusMessage;
559 if (confirmedAt != null) record.confirmedAt = confirmedAt;
560 });
582 - await bridgeTransfersStore.updateTransfer(record);
583 - }
584 -
585 - Future<void> _pollForSourceConfirmation(
586 - BridgeTransfer record,
587 - WalletBase wallet,
588 - ) async {
589 - final canceller = Completer<void>();
590 - _pollingCancellers[record.id] = canceller;
591 - final walletId = wallet.name;
592 - final deadline = DateTime.now().add(_pollTimeout);
561
562 try {
595 - while (DateTime.now().isBefore(deadline)) {
596 - await Future.any([
597 - Future.delayed(_pollInterval),
598 - canceller.future,
599 - ]);
600 -
601 - if (canceller.isCompleted || !_isValidWalletContext(walletId)) return;
602 -
603 - bool? receipt;
604 - try {
605 - receipt = await evm!.getTransactionReceipt(wallet, record.sourceTxHash);
606 - } catch (e) {
607 - printV('USDT0 bridge: Error fetching receipt: $e');
608 - continue;
609 - }
610 -
611 - if (receipt == null) continue;
612 -
613 - if (receipt == true) {
614 - await _updateTransferStatus(
615 - record,
616 - 'confirming',
617 - confirmedAt: DateTime.now(),
618 - );
619 -
620 - await Future.delayed(const Duration(seconds: 1));
621 - if (canceller.isCompleted || !_isValidWalletContext(walletId)) return;
622 -
623 - await _updateTransferStatus(record, 'initiated');
624 - _pollForDestinationCompletion(record, wallet);
625 -
626 - return;
627 - } else if (receipt == false) {
628 - await _updateTransferStatus(
629 - record,
630 - 'failed',
631 - errorMessage: 'Transaction reverted',
632 - );
633 - return;
634 - }
635 - }
636 -
637 - if (!_isValidWalletContext(walletId)) return;
638 -
639 - await _updateTransferStatus(record, 'initiated');
640 - _pollForDestinationCompletion(record, wallet);
641 - } finally {
642 - _pollingCancellers.remove(record.id);
563 + await bridgeTransfersStore.updateTransfer(record);
564 + } catch (e) {
565 + printV('USDT0 bridge: Error updating transfer status: $e');
566 }
567 }
568
646 - Future<void> _pollForDestinationCompletion(
569 + Future<void> _pollForConfirmation(
570 BridgeTransfer record,
648 - WalletBase wallet,
649 - ) async {
571 + WalletBase wallet, {
572 + required bool isSource,
573 + }) async {
574 final canceller = Completer<void>();
651 - _pollingCancellers['${record.id}_dest'] = canceller;
575 + final recordId = isSource ? record.id : '${record.id}_dest';
576 + final pollInterval = isSource ? _pollInterval : _destinationPollInterval;
577 + final pollTimeout = isSource ? _pollTimeout : _destinationPollTimeout;
578 final walletId = wallet.name;
653 - final deadline = DateTime.now().add(_destinationPollTimeout);
579 + final deadline = DateTime.now().add(pollTimeout);
580 +
581 + _pollingCancellers[recordId] = canceller;
582
583 try {
584 while (DateTime.now().isBefore(deadline)) {
585 await Future.any([
658 - Future.delayed(_destinationPollInterval),
586 + Future.delayed(pollInterval),
587 canceller.future,
588 ]);
589
590 if (canceller.isCompleted || !_isValidWalletContext(walletId)) return;
591
664 - LayerZeroMessageStatus? status;
665 - try {
666 - status = await LayerZeroScanService.getMessageStatus(record.sourceTxHash);
667 - } catch (e) {
668 - printV('USDT0 bridge: Error fetching LayerZero status: $e');
669 - continue;
670 - }
592 + if (isSource) {
593 + final receipt = await _fetchTransactionReceipt(record, wallet);
594
672 - if (status == null) continue;
595 + if (receipt != null) {
596 + final isTransactionSuccessful = receipt == true;
597 + await _updateTransferStatus(
598 + record,
599 + isTransactionSuccessful ? 'initiated' : 'failed',
600 + confirmedAt: isTransactionSuccessful ? DateTime.now() : null,
601 + errorMessage: !isTransactionSuccessful ? 'Transaction reverted' : null,
602 + );
603
674 - if (status.isDelivered) {
675 - await _updateTransferStatus(
676 - record,
677 - 'completed',
678 - statusMessage: status.status?.message,
679 - );
680 - return;
681 - }
604 + if (isTransactionSuccessful) {
605 + await Future.delayed(const Duration(seconds: 1));
606 + if (_isValidWalletContext(walletId)) {
607 + _pollForConfirmation(record, wallet, isSource: false);
608 + }
609 + }
610
683 - if (status.isFailed) {
684 - await _updateTransferStatus(
685 - record,
686 - 'failed',
687 - errorMessage: status.status?.message ?? 'Bridge message failed',
688 - statusMessage: status.status?.message,
689 - );
690 - return;
611 + return;
612 + }
613 +
614 + continue;
615 + } else {
616 + final status = await _fetchLayerZeroMessageStatus(record, wallet);
617 +
618 + if (status != null) {
619 + final statusMessage = _getStatusMessage(status, record);
620 + await _updateTransferStatus(
621 + record,
622 + statusMessage,
623 + errorMessage:
624 + status.isFailed ? status.status?.message ?? 'Bridge message failed' : null,
625 + statusMessage: status.status?.message,
626 + );
627 +
628 + if (status.isDelivered || status.isFailed) return;
629 + }
630 +
631 + continue;
632 }
633 + }
634
635 + if (isSource && _isValidWalletContext(walletId)) {
636 await _updateTransferStatus(
637 record,
695 - record.status,
696 - statusMessage: status.status?.message,
638 + 'failed',
639 + errorMessage: 'Source confirmation timed out',
640 );
641 }
642 + } catch (e) {
643 + printV('USDT0 bridge: Error polling for confirmation: $e');
644 } finally {
700 - _pollingCancellers.remove('${record.id}_dest');
645 + _pollingCancellers.remove(recordId);
646 }
647 }
648
649 + String _getStatusMessage(LayerZeroMessageStatus status, BridgeTransfer record) {
650 + if (status.isDelivered) {
651 + return 'completed';
652 + }
653 +
654 + if (status.isFailed) {
655 + return 'failed';
656 + }
657 +
658 + return record.status;
659 + }
660 +
661 + Future<bool?> _fetchTransactionReceipt(BridgeTransfer record, WalletBase wallet) async {
662 + try {
663 + return await evm!.getTransactionReceipt(wallet, record.sourceTxHash);
664 + } catch (e) {
665 + printV('USDT0 bridge: Error fetching receipt: $e');
666 + return null;
667 + }
668 + }
669 +
670 + Future<LayerZeroMessageStatus?> _fetchLayerZeroMessageStatus(
671 + BridgeTransfer record, WalletBase wallet) async {
672 + try {
673 + return await LayerZeroScanService.getMessageStatus(record.sourceTxHash);
674 + } catch (e) {
675 + printV('USDT0 bridge: Error fetching LayerZero status: $e');
676 + return null;
677 + }
678 + }
679 +
680 + @action
681 + void _clearQuoteState() {
682 + quote = null;
683 + quoteError = null;
684 + executeError = null;
685 + }
686 +
687 @action
705 - void clearBridgeSuccess() {
688 + void clearOnBridgeSuccess() {
689 amount = '';
690 recipientAddress = '';
691 destinationWalletName = null;
@@ -712,7 +695,25 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
695 _clearQuoteState();
696 }
697
698 + void _cancelAllPolling() {
699 + for (final canceller in _pollingCancellers.values) {
700 + if (!canceller.isCompleted) {
701 + canceller.complete();
702 + }
703 + }
704 + _pollingCancellers.clear();
705 + }
706 +
707 void dispose() {
708 _cancelAllPolling();
709 }
710 }
711 +
712 +extension CryptoCurrencyX on CryptoCurrency? {
713 + Erc20Token? get asErc20 {
714 + final token = this;
715 + return token is Erc20Token ? token : null;
716 + }
717 +
718 + bool get isNotErc20 => this == null || this is! Erc20Token;
719 +}
tool/configure.dart
+12 -2
@@ -1570,7 +1570,7 @@ abstract class EVM {
1570 bool isUSDT0Token(WalletBase wallet, CryptoCurrency token);
1571 List<ChainInfo> getUSDT0DestinationChains(WalletBase wallet);
1572
1573 - Future<USDT0Quote> quoteUSDT0Transfer({
1573 + Future<BridgeQuote> quoteUSDT0Transfer({
1574 required WalletBase wallet,
1575 required int sourceChainId,
1576 required int destinationChainId,
@@ -1585,7 +1585,7 @@ abstract class EVM {
1585 required int destinationChainId,
1586 required BigInt amount,
1587 required String recipientAddress,
1588 - required USDT0Quote quote,
1588 + required BridgeQuote quote,
1589 required TransactionPriority priority,
1590 bool useBlinkProtection = true,
1591 });
@@ -1631,6 +1631,16 @@ class EvmWalletConnectFeeQuote {
1631 final int maxPriorityFeePerGasWei;
1632 final int? latestBaseFeeWei;
1633 }
1634 +
1635 +class BridgeQuote {
1636 + const BridgeQuote({
1637 + required this.nativeFee,
1638 + required this.lzTokenFee,
1639 + });
1640 +
1641 + final BigInt nativeFee;
1642 + final BigInt lzTokenFee;
1643 +}
1644 """;
1645
1646 const evmEmptyDefinition = 'EVM? evm;\n';