CW-1273: Implement USDT0 Bridge (#2855)

* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* feat: Implement USDT0 Bridging using OFT Standard by Layer Zero. POC available in apps menu.

* Add transaction history and status polling for USDT0 bridging

* Add  Transaction history and status polling for USDT0 Bridging

* USDT0 Bridging Implementation

* Update Arbitrum USDT token symbol

* fix: Merge conflicts

* fix: Merge conflicts

* chore: Rever formatting

* feat: Implement new flow

* feat: Implement new ui flow

* Add currency to configure

* feat: new ui flow

* Update USDT0 implementation
- Switch to sqlite for storage instead of hive
- Switch bridge history and details to modals
- Switch bridge transfer details page to new tx details ui

* refactor: rearrange modal action buttons

* enhance usdt0 bridge flows

* Update lib/entities/wallet_manager.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2026-04-15 05:06:05 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 162e0a5300
commit 586a040e4a
45 changed files with 3958 additions and 43 deletions
@@ -0,0 +1,17 @@
import 'package:cw_core/wallet_info.dart';
class BridgeReceivingWalletOption {
const BridgeReceivingWalletOption({
required this.walletInfo,
required this.isCurrent,
this.groupLabel,
});
final WalletInfo walletInfo;
final bool isCurrent;
final String? groupLabel;
String get name => walletInfo.name;
String get address => walletInfo.address;
}
@@ -0,0 +1,719 @@
import 'dart:async';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
import 'package:cake_wallet/entities/bridge_transfer.dart';
import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/core/layerzero_scan_service.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
part 'bridge_view_model.g.dart';
class BridgeViewModel = BridgeViewModelBase with _$BridgeViewModel;
abstract class BridgeViewModelBase extends WalletChangeListenerViewModel with Store {
BridgeViewModelBase({
required AppStore appStore,
required this.bridgeTransfersStore,
required this.walletManager,
required this.fiatConversionStore,
required this.settingsStore,
}) : _appStore = appStore,
super(appStore: appStore);
final AppStore _appStore;
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;
@observable
ObservableList<BridgeReceivingWalletOption> bridgeReceivingWalletOptions =
ObservableList<BridgeReceivingWalletOption>();
@observable
bool isBridgeReceivingWalletListLoading = false;
@observable
CryptoCurrency? selectedToken;
@observable
int? destinationChainId;
@observable
String amount = '';
@observable
String recipientAddress = '';
@observable
String? destinationWalletName;
@observable
USDT0Quote? quote;
@observable
bool isQuoteLoading = false;
@observable
String? quoteError;
@observable
bool isExecuting = false;
@observable
String? executeError;
@observable
bool bridgeSuccess = false;
@observable
BridgeTransfer? lastCreatedBridgeTransfer;
@computed
int? get sourceChainId => evm!.getSelectedChainId(wallet);
@computed
String get sourceAddress => wallet.walletAddresses.address;
@computed
List<ChainInfo> get availableDestinationChains {
if (!isEVMCompatibleChain(wallet.type)) return [];
return evm!.getUSDT0DestinationChains(wallet);
}
@computed
List<Erc20Token> get availableUSDT0Tokens {
if (!isEVMCompatibleChain(wallet.type)) return [];
final tokens = wallet.balance.keys.whereType<Erc20Token>();
return tokens.where((token) => evm!.isUSDT0Token(wallet, token)).toList(growable: false);
}
@computed
ChainInfo? get destinationChainInfo {
if (destinationChainId == null) return null;
return availableDestinationChains.firstWhereOrNull((c) => c.chainId == destinationChainId);
}
@computed
String get tokenBalanceFormatted {
final token = selectedToken;
if (token is! Erc20Token) return '0.00';
return amountParsingProxy.getDisplayCryptoStringFromBigInt(
selectedTokenBalance,
token,
);
}
@computed
String get amountDisplayFormatted {
if (amount.isEmpty) return '';
final token = selectedToken;
if (token is! Erc20Token) return amount.replaceAll(',', '.');
return amountParsingProxy.getDisplayCryptoAmount(
amount.replaceAll(',', '.'),
token,
);
}
DecimalAmountValidator get decimalAmountValidator => DecimalAmountValidator(
currency: selectedToken!,
isAutovalidate: true,
);
@computed
String get fiatAmountFormatted {
if (amount.isEmpty) return '';
final token = selectedToken;
if (token is! Erc20Token) return '';
final price = fiatConversionStore.prices[token];
if (price == null) return '';
final forFiat = amountParsingProxy.getDisplayCryptoAmount(
amount.replaceAll(',', '.'),
token,
);
return calculateFiatAmount(
price: price,
cryptoAmount: forFiat,
);
}
@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,
);
}
@computed
String get quoteNativeFeeFormattedForDisplay {
if (quoteNativeFee.isEmpty) return '';
return '${quoteNativeFee} ${wallet.currency.title}';
}
@computed
String get quoteNativeFiatFeeFormattedForDisplay {
if (quote == null || quoteNativeFee.isEmpty) return '';
final price = fiatConversionStore.prices[wallet.currency];
if (price == null) return '';
final fiatFeeFormatted = calculateFiatAmount(
price: price,
cryptoAmount: amountParsingProxy.getDisplayCryptoAmount(
quoteNativeFee.replaceAll(',', '.'),
wallet.currency,
),
);
return '(${settingsStore.fiatCurrency.title} $fiatFeeFormatted)';
}
@computed
bool get canProceedToDestinationNetwork {
if (amount.isEmpty) return false;
if (selectedToken == null || selectedToken is! Erc20Token) return false;
if (amountError != null) return false;
final token = selectedToken as Erc20Token;
final validAmount = amountParsingProxy.tryParseCryptoString(
amount.replaceAll(',', '.'),
token,
);
return validAmount != null && validAmount > BigInt.zero;
}
@action
void applyInitialBridgeToken(CryptoCurrency asset) {
for (final t in availableUSDT0Tokens) {
if (t == asset) {
setSelectedToken(t);
break;
}
}
}
@action
void setDestinationChain(int chainId) {
destinationChainId = chainId;
_clearQuoteState();
}
@action
void setSelectedToken(CryptoCurrency token) {
selectedToken = token;
_clearQuoteState();
}
@action
void setAmount(String value) {
amount = value;
_clearQuoteState();
}
@action
void setMaxAmount() {
final token = selectedToken;
if (token is! Erc20Token) return;
if (selectedTokenBalance == BigInt.zero) {
setAmount('');
return;
}
setAmount(
amountParsingProxy.getDisplayCryptoStringFromBigInt(
selectedTokenBalance,
token,
),
);
}
@action
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,
);
runInAction(() {
fiatConversionStore.prices[crypto] = p;
});
}
@action
Future<void> ensureFiatPriceForSelectedToken() async {
await _ensureFiatPriceFor(selectedToken!);
}
@action
Future<void> ensureFiatPriceForNativeCurrency() async {
await _ensureFiatPriceFor(wallet.currency);
}
@computed
BigInt get selectedTokenBalance {
final bal = wallet.balance[selectedToken];
if (bal is EVMChainERC20Balance) return bal.balance;
return 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;
final destWalletType = evm!.getWalletTypeByChainId(destinationChainId!);
isBridgeReceivingWalletListLoading = true;
try {
await walletManager.updateWalletGroups();
final all = await WalletInfo.getAll();
if (destWalletType == null) {
bridgeReceivingWalletOptions.clear();
return;
}
final filtered =
all.where((w) => w.type == destWalletType && w.hardwareWalletType == null).toList();
final options = <BridgeReceivingWalletOption>[];
for (final wi in filtered) {
final isCurrent = wi.name == wallet.name;
options.add(
BridgeReceivingWalletOption(
walletInfo: wi,
isCurrent: isCurrent,
groupLabel: walletManager.getGroupName(wi),
),
);
}
bridgeReceivingWalletOptions
..clear()
..addAll(options);
} finally {
isBridgeReceivingWalletListLoading = false;
}
}
String? missingBridgeFieldsMessage() {
final src = sourceChainId;
final dst = destinationChainId;
final token = selectedToken;
if (src == null ||
dst == null ||
token == null ||
amount.isEmpty ||
recipientAddress.trim().isEmpty) {
return 'Fill all fields';
}
return null;
}
({String? error, BigInt? parsedAmount}) _parseAndValidateAmount(Erc20Token token) {
final amountBigInt = amountParsingProxy.tryParseCryptoString(
amount.replaceAll(',', '.'),
token,
);
if (amountBigInt == null || amountBigInt == BigInt.zero) {
return (error: 'Invalid amount', parsedAmount: null);
}
if (amountBigInt > selectedTokenBalance) {
return (
error: 'Insufficient balance for ${token.title} token.',
parsedAmount: null,
);
}
return (error: null, parsedAmount: amountBigInt);
}
@action
Future<void> loadQuote() async {
final missing = missingBridgeFieldsMessage();
if (missing != null) {
quoteError = missing;
return;
}
final token = selectedToken!;
if (token is! Erc20Token) return;
final check = _parseAndValidateAmount(token);
if (check.error != null) {
quoteError = check.error;
return;
}
final src = sourceChainId!;
final dst = destinationChainId!;
final amountBigInt = check.parsedAmount!;
isQuoteLoading = true;
quoteError = null;
quote = null;
executeError = null;
try {
quote = await evm!.quoteUSDT0Transfer(
wallet: wallet,
sourceChainId: src,
destinationChainId: dst,
amount: amountBigInt,
recipientAddress: recipientAddress.trim(),
);
} catch (e) {
quoteError = e.toString();
} finally {
isQuoteLoading = false;
}
}
@action
Future<void> executeBridge() async {
if (quote == null) {
executeError = 'Get a quote first';
return;
}
final missing = missingBridgeFieldsMessage();
if (missing != null) {
executeError = missing;
return;
}
final token = selectedToken!;
if (token is! Erc20Token) return;
final check = _parseAndValidateAmount(token);
if (check.error != null) {
executeError = check.error;
return;
}
final src = sourceChainId!;
final dst = destinationChainId!;
final amountBigInt = check.parsedAmount!;
isExecuting = true;
executeError = null;
try {
final priority = EVMChainTransactionPriority.medium;
final pending = await evm!.executeUSDT0Transfer(
wallet: wallet,
token: token,
sourceChainId: src,
destinationChainId: dst,
amount: amountBigInt,
recipientAddress: recipientAddress.trim(),
quote: quote!,
priority: priority as TransactionPriority,
useBlinkProtection: canSupportBlinkProtection(src),
);
final sourceTxHash = pending.evmTxHashFromRawHex ?? pending.id;
await pending.commit();
final record = BridgeTransfer(
id: '${sourceTxHash}_${DateTime.now().millisecondsSinceEpoch}',
walletId: wallet.name,
sourceChainId: src,
destinationChainId: dst,
tokenSymbol: token.title,
tokenContract: token.contractAddress,
amount: amount,
recipientAddress: recipientAddress.trim(),
sourceTxHash: sourceTxHash,
status: 'submitted',
createdAt: DateTime.now(),
);
await bridgeTransfersStore.addTransfer(record);
runInAction(() {
quote = null;
bridgeSuccess = true;
lastCreatedBridgeTransfer = record;
});
onBridgeSuccess?.call();
_pollForSourceConfirmation(record, wallet);
} catch (e) {
executeError = e.toString();
} finally {
isExecuting = false;
}
}
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;
final activeTransfers = bridgeTransfersStore.bridgeTransfers
.where((t) => t.walletId == wallet.name && t.isActive)
.toList();
for (final transfer in activeTransfers) {
if (transfer.status == 'submitted' || transfer.status == 'confirming') {
_pollForSourceConfirmation(transfer, wallet);
} else if (transfer.status == 'initiated') {
_pollForDestinationCompletion(transfer, wallet);
}
}
}
bool _isValidWalletContext(String expectedWalletId) {
return wallet.name == expectedWalletId &&
isEVMCompatibleChain(wallet.type) &&
!_pollingCancellers.values.any((c) => c.isCompleted);
}
Future<void> _updateTransferStatus(
BridgeTransfer record,
String status, {
String? errorMessage,
String? statusMessage,
DateTime? confirmedAt,
}) async {
if (!_isValidWalletContext(record.walletId)) return;
runInAction(() {
record.updatedAt = DateTime.now();
record.status = status;
if (errorMessage != null) record.errorMessage = errorMessage;
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);
}
}
Future<void> _pollForDestinationCompletion(
BridgeTransfer record,
WalletBase wallet,
) async {
final canceller = Completer<void>();
_pollingCancellers['${record.id}_dest'] = canceller;
final walletId = wallet.name;
final deadline = DateTime.now().add(_destinationPollTimeout);
try {
while (DateTime.now().isBefore(deadline)) {
await Future.any([
Future.delayed(_destinationPollInterval),
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');
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;
}
await _updateTransferStatus(
record,
record.status,
statusMessage: status.status?.message,
);
}
} finally {
_pollingCancellers.remove('${record.id}_dest');
}
}
@action
void clearBridgeSuccess() {
amount = '';
recipientAddress = '';
destinationWalletName = null;
destinationChainId = null;
bridgeSuccess = false;
lastCreatedBridgeTransfer = null;
_clearQuoteState();
}
void dispose() {
_cancelAllPolling();
}
}
@@ -0,0 +1,181 @@
import 'dart:async';
import 'package:cake_wallet/entities/bridge_transfer.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
part 'bridge_details_view_model.g.dart';
class BridgeDetailsViewModel = BridgeDetailsViewModelBase with _$BridgeDetailsViewModel;
abstract class BridgeDetailsViewModelBase with Store {
BridgeDetailsViewModelBase({
required BridgeTransfer transferForDetails,
required this.bridgeTransfersStore,
required this.walletId,
}) : items = ObservableList<TransactionDetailsListItem>(),
transfer = _findTransferInStore(
bridgeTransfersStore.bridgeTransfers, transferForDetails.id, walletId) ??
transferForDetails {
_updateItems();
_setupReaction();
}
static BridgeTransfer? _findTransferInStore(
List<BridgeTransfer> transfers,
String transferId,
String walletId,
) {
try {
return transfers.firstWhere(
(t) => t.id == transferId && t.walletId == walletId,
);
} catch (_) {
return null;
}
}
final BridgeTransfersStore bridgeTransfersStore;
final String walletId;
ReactionDisposer? _reactionDisposer;
@observable
BridgeTransfer transfer;
@observable
ObservableList<TransactionDetailsListItem> items;
Timer? timer;
void _setupReaction() {
_reactionDisposer = reaction(
(_) => bridgeTransfersStore.bridgeTransfers,
(_) => updateTransfer(),
);
updateTransfer();
}
@action
void updateTransfer() {
final updatedTransfer = _findTransferInStore(
bridgeTransfersStore.bridgeTransfers,
transfer.id,
walletId,
);
if (updatedTransfer != null) {
transfer = updatedTransfer;
_updateItems();
}
}
void dispose() {
_reactionDisposer?.call();
timer?.cancel();
}
void _updateItems() {
items.clear();
final statusText = transfer.statusMessage?.isNotEmpty == true
? '${_statusLabel(transfer.status)} · ${transfer.statusMessage}'
: _statusLabel(transfer.status);
items.add(
DetailsListStatusItem(
title: "Status",
value: statusText,
status: transfer.status,
),
);
final sourceName =
evm?.getChainNameByChainId(transfer.sourceChainId) ?? '${transfer.sourceChainId}';
final destName =
evm?.getChainNameByChainId(transfer.destinationChainId) ?? '${transfer.destinationChainId}';
items.add(
StandartListItem(
title: "Source chain",
value: sourceName,
),
);
items.add(
StandartListItem(
title: "Destination chain",
value: destName,
),
);
items.add(
StandartListItem(
title: "Amount",
value: '${transfer.amount} ${transfer.tokenSymbol}',
),
);
items.add(
AddressListItem(
title: "Recipient",
value: transfer.recipientAddress,
),
);
final sourceExplorerUrl = evm?.getExplorerUrlForChainId(transfer.sourceChainId);
final sourceTxUrl = sourceExplorerUrl != null && sourceExplorerUrl.isNotEmpty
? '$sourceExplorerUrl/tx/${transfer.sourceTxHash}'
: null;
if (sourceTxUrl != null) {
final explorerDescription = S.current.view_transaction_on + Uri.parse(sourceTxUrl).host;
items.add(
TrackTradeListItem(
title: explorerDescription,
value: sourceTxUrl,
onTap: () => _launchUrl(sourceTxUrl),
),
);
}
if (transfer.errorMessage != null && transfer.errorMessage!.isNotEmpty) {
items.add(
StandartListItem(
title: "Error",
value: transfer.errorMessage!,
),
);
}
}
String _statusLabel(String status) {
switch (status) {
case 'submitted':
return "Submitted";
case 'confirming':
return "Confirming on source";
case 'initiated':
return "Bridge initiated";
case 'completed':
return "Completed";
case 'failed':
return "Failed";
default:
return status;
}
}
void _launchUrl(String url) {
final uri = Uri.parse(url);
try {
launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {}
}
}
@@ -0,0 +1,37 @@
import 'package:cake_wallet/entities/bridge_transfer.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:mobx/mobx.dart';
part 'bridge_history_view_model.g.dart';
class BridgeHistoryViewModel = BridgeHistoryViewModelBase with _$BridgeHistoryViewModel;
abstract class BridgeHistoryViewModelBase with Store {
BridgeHistoryViewModelBase({
required this.bridgeTransfersStore,
required this.appStore,
});
final BridgeTransfersStore bridgeTransfersStore;
final AppStore appStore;
@computed
List<BridgeTransfer> get walletTransfers {
final wallet = appStore.wallet;
if (wallet == null) return [];
return bridgeTransfersStore.bridgeTransfers.where((t) => t.walletId == wallet.name).toList();
}
@computed
List<BridgeTransfer> get activeTransfers =>
walletTransfers.where((b) => b.isActive).toList(growable: false);
@computed
List<BridgeTransfer> get pastTransfers =>
walletTransfers.where((b) => !b.isActive).toList(growable: false);
@computed
bool get isEmpty => walletTransfers.isEmpty;
}
@@ -393,6 +393,14 @@ abstract class DashboardViewModelBase with Store {
return false;
}
bool showBridge(CryptoCurrency currency) {
if(!isEVMCompatibleChain(wallet.type)) return false;
if(evm!.isUSDT0Token(wallet, currency)) return true;
return false;
}
@action
Future<void> loadCardDesigns() async {
final accountStyleSettings =