mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
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:
co-authored by
Omar Hatem
parent
928113ea25
commit
40c4118b66
@@ -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
@@ -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
@@ -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
@@ -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';
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user