Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-05-01 17:32:21 +02:00
committed by GitHub
15 changed files with 414 additions and 295 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';
-2
View File
@@ -401,8 +401,6 @@ class _NewSwapPageState extends State<NewSwapPage> {
address: isCurrentTypeWallet
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
+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';
@@ -636,8 +636,6 @@ class ExchangePage extends BasePage {
key.currentState!.changeAddress(
address:
isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
+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;
}
+138 -19
View File
@@ -48,6 +48,7 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/utils/token_utilities.dart';
@@ -250,6 +251,20 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
return true;
});
} else if (isEVMCompatibleChain(wallet.type)) {
final currency = depositCurrency;
final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
(c) =>
c.title == depositCurrency.title &&
(c.tag == depositCurrency.tag || c.tag == depositCurrency.title),
);
final balanceForCurrency =
balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
if (depositCurrency == currency && balanceForCurrency != null) {
depositAvailableAmount = _appStore.amountParsingProxy
.getDisplayCryptoStringFromBigInt(
balanceForCurrency.fullAvailableBalance, balanceCurrency!);
}
} else {
final currency = depositCurrency;
final amount = _appStore.amountParsingProxy.getDisplayCryptoString(
@@ -478,9 +493,20 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@computed
String? get balanceDisplay {
final bal = wallet.balance[depositCurrency]?.fullAvailableBalance;
if(bal == null) return null;
return amountParsingProxy.getDisplayCryptoStringFromBigInt(bal, depositCurrency);
CryptoCurrency? balanceCurrency;
if (isEVMCompatibleChain(wallet.type)) {
balanceCurrency = wallet.balance.keys.firstWhereOrNull(
(c) =>
c.title == depositCurrency.title &&
(c.tag == depositCurrency.tag || c.tag == depositCurrency.title),
);
} else {
balanceCurrency = depositCurrency;
}
final bal = balanceCurrency != null ? wallet.balance[balanceCurrency]?.fullAvailableBalance : null;
if (bal == null) return null;
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
bal, balanceCurrency ?? depositCurrency);
}
//* Still open to further optimize these checks
@@ -515,15 +541,23 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return priority;
}
bool get hasAllAmount =>
[
WalletType.monero,
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.dogecoin,
].contains(wallet.type) &&
(depositCurrency == wallet.currency);
bool get hasAllAmount {
if ([
WalletType.monero,
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.dogecoin,
].contains(wallet.type)) return (depositCurrency == wallet.currency);
if (!isEVMCompatibleChain(wallet.type)) return false;
if (depositCurrency == wallet.currency) return true;
return wallet.balance.keys.any((c) =>
c.title == depositCurrency.title &&
(c.tag == depositCurrency.tag || c.tag == depositCurrency.title));
}
bool get isMoneroWallet => wallet.type == WalletType.monero;
@@ -706,26 +740,38 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@action
void changeDepositCurrency({required CryptoCurrency currency}) {
final previousCurrency = depositCurrency;
final wasSendAllEnabled = isSendAllEnabled;
depositCurrency = currency;
isFixedRateMode = false;
_onPairChange();
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
if (previousCurrency != currency) {
_onPairChange(clearBoth: wasSendAllEnabled);
}
fetchFiatPrice(currency);
updateDepositAvailableAmount();
}
@action
void changeReceiveCurrency({required CryptoCurrency currency}) {
final previousCurrency = receiveCurrency;
if (!(currency.tag == receiveCurrency.tag ||
currency.tag == receiveCurrency.title ||
currency.title == receiveCurrency.tag ||
currency == receiveCurrency)) {
receiveAddress = "";
}
receiveCurrency = currency;
isFixedRateMode = false;
_onPairChange();
isDepositAddressEnabled = !useSameWalletAddress(depositCurrency);
if (previousCurrency != currency) {
_onPairChange();
}
fetchFiatPrice(currency);
}
@@ -836,6 +882,11 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
isReceiveAmount: isFixedRateMode);
}
bool _excludeProviderForSwapAll(ExchangeProvider provider) =>
isSendAllEnabled &&
(provider.description == ExchangeProviderDescription.swapsXyz ||
provider.description == ExchangeProviderDescription.nearIntents);
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
@@ -846,6 +897,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
if (_excludeProviderForSwapAll(provider)) return false;
final limits = _providerLimits[provider];
if (limits == null) return false;
@@ -962,7 +1015,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
if (lowestMin != double.maxFinite) {
limits = Limits(min: lowestMin, max: highestMax);
limitsState = LimitsLoadedSuccessfully(limits: limits);
} else {
limitsState = LimitsLoadedFailure(error: 'Limits loading failed');
@@ -1081,6 +1133,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
printV('createTrade: trying provider=${provider.title}');
if (_excludeProviderForSwapAll(provider)) {
printV('Skipping Swaps.xyz or Near Intents for swap all');
continue;
}
// should not happen but just as an extra check
if (isFixedRateMode && provider.supportsFixedRate == false) {
continue;
@@ -1254,6 +1310,56 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final amount = await unspentCoinsListViewModel.getSendingBalance(UnspentCoinType.any);
changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.from(amount)), isCanonical: true);
} else if (isEVMCompatibleChain(wallet.type)) {
final balanceCurrency = wallet.balance.keys.firstWhereOrNull(
(currency) =>
currency.title == depositCurrency.title &&
(currency.tag == depositCurrency.tag || currency.tag == depositCurrency.title),
);
final balanceForCurrency = balanceCurrency != null ? wallet.balance[balanceCurrency] : null;
if (balanceForCurrency == null) {
changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
return;
}
final balanceAmount = _appStore.amountParsingProxy.getDisplayCryptoStringFromBigInt(balanceForCurrency.fullAvailableBalance, balanceCurrency!);
final balanceDouble = double.tryParse(balanceAmount.replaceAll(',', '.')) ?? 0.0;
if (balanceDouble <= 0) {
changeDepositAmount(amount: wallet.currency.formatAmount(BigInt.zero), isCanonical: true);
return;
}
final isNative = depositCurrency == wallet.currency;
if (!isNative) {
changeDepositAmount(amount: balanceAmount, isCanonical: true);
return;
}
try {
final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId);
await wallet.updateEstimatedFeesParams(priority);
String? feeString;
if (isEVMCompatibleChain(wallet.type)) {
feeString = evm!.getEVMNativeEstimatedFee(wallet);
}
if (feeString == null || feeString.isEmpty) {
changeDepositAmount(amount: balanceAmount, isCanonical: true);
return;
}
final balanceWei = balanceForCurrency.fullAvailableBalance;
final feeWei = BigInt.parse(feeString);
final amountAfterFeeWei = balanceWei > feeWei ? balanceWei - feeWei : BigInt.zero;
changeDepositAmount(
amount: wallet.currency.formatAmount(amountAfterFeeWei), isCanonical: true);
} catch (e) {
printV('Error calculating send all for EVM: $e');
changeDepositAmount(amount: balanceAmount, isCanonical: true);
}
}
}
@@ -1293,13 +1399,26 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
void removeTemplate({required ExchangeTemplate template}) =>
_exchangeTemplateStore.remove(template: template);
void _onPairChange() {
_depositAmount = '';
_receiveAmount = '';
void _onPairChange({bool clearBoth = false}) {
if (clearBoth) {
_depositAmount = '';
_receiveAmount = '';
if (!hasAllAmount) isSendAllEnabled = false;
} else {
if (isFixedRateMode) {
_depositAmount = '';
} else {
_receiveAmount = '';
}
}
bestRate = 0.0;
bestRateProvider = null;
_sortedAvailableProviders.clear();
loadLimits();
loadLimits().then((_) {
if (clearBoth && hasAllAmount) {
calculateDepositAllAmount();
}
});
_setAvailableProviders();
}
+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';