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';