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>
This commit is contained in:
David Adegoke
2026-04-30 20:15:11 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 928113ea25
commit 40c4118b66
12 changed files with 276 additions and 272 deletions
+1 -1
View File
@@ -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._();
@@ -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) {
+2 -2
View File
@@ -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';
+12 -8
View File
@@ -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<USDT0Quote> quoteUSDT0Transfer({
Future<BridgeQuote> 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<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
WalletBase wallet,
TransactionPriority priority,
@@ -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<BridgeAmountPage> {
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<BridgeAmountPage> {
bridgeViewModel.onBridgeSuccess = null;
bridgeViewModel.setAmount('');
_amountController.dispose();
bridgeViewModel.dispose();
super.dispose();
}
@@ -165,7 +166,9 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
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<BridgeAmountPage> {
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)
],
);
},
@@ -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';
@@ -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';
+1 -1
View File
@@ -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';
+234 -233
View File
@@ -44,11 +44,16 @@ abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with St
AmountParsingProxy get amountParsingProxy => _appStore.amountParsingProxy;
void Function()? onBridgeSuccess;
final Map<String, Completer<void>> _pollingCancellers = {};
final BridgeTransfersStore bridgeTransfersStore;
final WalletManager walletManager;
final FiatConversionStore fiatConversionStore;
final SettingsStore settingsStore;
final FiatConversionStore fiatConversionStore;
final BridgeTransfersStore bridgeTransfersStore;
final Map<String, Completer<void>> _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<BridgeReceivingWalletOption> 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<ChainInfo> 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<void> _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<void> 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<void> _pollForSourceConfirmation(
BridgeTransfer record,
WalletBase wallet,
) async {
final canceller = Completer<void>();
_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<void> _pollForDestinationCompletion(
Future<void> _pollForConfirmation(
BridgeTransfer record,
WalletBase wallet,
) async {
WalletBase wallet, {
required bool isSource,
}) async {
final canceller = Completer<void>();
_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<bool?> _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<LayerZeroMessageStatus?> _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;
}
+12 -2
View File
@@ -1570,7 +1570,7 @@ abstract class EVM {
bool isUSDT0Token(WalletBase wallet, CryptoCurrency token);
List<ChainInfo> getUSDT0DestinationChains(WalletBase wallet);
Future<USDT0Quote> quoteUSDT0Transfer({
Future<BridgeQuote> 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';