mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
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:
co-authored by
Omar Hatem
parent
162e0a5300
commit
586a040e4a
@@ -0,0 +1,318 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
|
||||
class LayerZeroScanService {
|
||||
static const String _baseUrl = 'https://scan.layerzero-api.com/v1';
|
||||
|
||||
static Future<LayerZeroMessageStatus?> getMessageStatus(
|
||||
String sourceTxHash,
|
||||
) async {
|
||||
try {
|
||||
final uri = Uri.parse('$_baseUrl/messages/tx/$sourceTxHash');
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode != 200) return null;
|
||||
|
||||
final decoded = json.decode(response.body);
|
||||
|
||||
Map<String, dynamic>? data;
|
||||
if (decoded is List && decoded.isNotEmpty) {
|
||||
data = decoded.first as Map<String, dynamic>?;
|
||||
} else if (decoded is Map<String, dynamic>) {
|
||||
if (decoded.containsKey('data') && decoded['data'] is List) {
|
||||
final dataList = decoded['data'] as List;
|
||||
if (dataList.isNotEmpty) {
|
||||
data = dataList.first as Map<String, dynamic>?;
|
||||
}
|
||||
} else {
|
||||
data = decoded;
|
||||
}
|
||||
}
|
||||
if (data == null) return null;
|
||||
return LayerZeroMessageStatus.fromJson(data);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroMessageStatus {
|
||||
final String? guid;
|
||||
final LayerZeroStatus? status;
|
||||
final LayerZeroSource? source;
|
||||
final LayerZeroDestination? destination;
|
||||
final LayerZeroVerification? verification;
|
||||
|
||||
LayerZeroMessageStatus({
|
||||
this.guid,
|
||||
this.status,
|
||||
this.source,
|
||||
this.destination,
|
||||
this.verification,
|
||||
});
|
||||
|
||||
factory LayerZeroMessageStatus.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroMessageStatus(
|
||||
guid: json['guid'] as String?,
|
||||
status: json['status'] != null
|
||||
? LayerZeroStatus.fromJson(
|
||||
json['status'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
source: json['source'] != null
|
||||
? LayerZeroSource.fromJson(
|
||||
json['source'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
destination: json['destination'] != null
|
||||
? LayerZeroDestination.fromJson(
|
||||
json['destination'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
verification: json['verification'] != null
|
||||
? LayerZeroVerification.fromJson(
|
||||
json['verification'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isDelivered =>
|
||||
status?.name == 'DELIVERED' ||
|
||||
destination?.status == 'DELIVERED' ||
|
||||
destination?.status == 'SUCCEEDED';
|
||||
|
||||
bool get isFailed => status?.name == 'FAILED' || destination?.status == 'FAILED';
|
||||
|
||||
bool get isInflight =>
|
||||
status?.name == 'INFLIGHT' ||
|
||||
status?.name == 'CONFIRMING' ||
|
||||
destination?.status == 'WAITING' ||
|
||||
destination?.status == 'VALIDATING_TX' ||
|
||||
(destination?.status != 'SUCCEEDED' &&
|
||||
destination?.status != 'DELIVERED' &&
|
||||
destination?.status != 'FAILED' &&
|
||||
destination?.tx == null);
|
||||
}
|
||||
|
||||
class LayerZeroStatus {
|
||||
final String? name;
|
||||
final String? message;
|
||||
|
||||
LayerZeroStatus({this.name, this.message});
|
||||
|
||||
factory LayerZeroStatus.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroStatus(
|
||||
name: json['name'] as String?,
|
||||
message: json['message'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroSource {
|
||||
final String? status;
|
||||
final LayerZeroTransaction? tx;
|
||||
|
||||
LayerZeroSource({
|
||||
this.status,
|
||||
this.tx,
|
||||
});
|
||||
|
||||
factory LayerZeroSource.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroSource(
|
||||
status: json['status'] as String?,
|
||||
tx: json['tx'] != null
|
||||
? LayerZeroTransaction.fromJson(
|
||||
json['tx'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroDestination {
|
||||
final String? status;
|
||||
final LayerZeroTransaction? tx;
|
||||
final LayerZeroNativeDrop? nativeDrop;
|
||||
final LayerZeroLzCompose? lzCompose;
|
||||
|
||||
LayerZeroDestination({
|
||||
this.status,
|
||||
this.tx,
|
||||
this.nativeDrop,
|
||||
this.lzCompose,
|
||||
});
|
||||
|
||||
factory LayerZeroDestination.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroDestination(
|
||||
status: json['status'] as String?,
|
||||
tx: json['tx'] != null
|
||||
? LayerZeroTransaction.fromJson(
|
||||
json['tx'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
nativeDrop: json['nativeDrop'] != null
|
||||
? LayerZeroNativeDrop.fromJson(
|
||||
json['nativeDrop'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
lzCompose: json['lzCompose'] != null
|
||||
? LayerZeroLzCompose.fromJson(
|
||||
json['lzCompose'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroNativeDrop {
|
||||
final String? status;
|
||||
|
||||
LayerZeroNativeDrop({this.status});
|
||||
|
||||
factory LayerZeroNativeDrop.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroNativeDrop(
|
||||
status: json['status'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroLzCompose {
|
||||
final String? status;
|
||||
|
||||
LayerZeroLzCompose({this.status});
|
||||
|
||||
factory LayerZeroLzCompose.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroLzCompose(
|
||||
status: json['status'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroTransaction {
|
||||
final String? txHash;
|
||||
final String? blockHash;
|
||||
final String? blockNumber;
|
||||
final int? blockTimestamp;
|
||||
final String? from;
|
||||
|
||||
LayerZeroTransaction({
|
||||
this.txHash,
|
||||
this.blockHash,
|
||||
this.blockNumber,
|
||||
this.blockTimestamp,
|
||||
this.from,
|
||||
});
|
||||
|
||||
factory LayerZeroTransaction.fromJson(Map<String, dynamic> json) {
|
||||
final blockNumberValue = json['blockNumber'];
|
||||
final blockNumberStr = blockNumberValue != null
|
||||
? (blockNumberValue is int ? blockNumberValue.toString() : blockNumberValue as String?)
|
||||
: null;
|
||||
return LayerZeroTransaction(
|
||||
txHash: json['txHash'] as String?,
|
||||
blockHash: json['blockHash'] as String?,
|
||||
blockNumber: blockNumberStr,
|
||||
blockTimestamp: json['blockTimestamp'] as int?,
|
||||
from: json['from'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroVerification {
|
||||
final LayerZeroDvn? dvn;
|
||||
final LayerZeroSealer? sealer;
|
||||
|
||||
LayerZeroVerification({
|
||||
this.dvn,
|
||||
this.sealer,
|
||||
});
|
||||
|
||||
factory LayerZeroVerification.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroVerification(
|
||||
dvn: json['dvn'] != null
|
||||
? LayerZeroDvn.fromJson(
|
||||
json['dvn'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
sealer: json['sealer'] != null
|
||||
? LayerZeroSealer.fromJson(
|
||||
json['sealer'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroDvn {
|
||||
final Map<String, LayerZeroDvnStatus>? dvns;
|
||||
final String? status;
|
||||
|
||||
LayerZeroDvn({
|
||||
this.dvns,
|
||||
this.status,
|
||||
});
|
||||
|
||||
factory LayerZeroDvn.fromJson(Map<String, dynamic> json) {
|
||||
final dvnsMap = json['dvns'] as Map<String, dynamic>?;
|
||||
final parsedDvns = dvnsMap?.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
LayerZeroDvnStatus.fromJson(value as Map<String, dynamic>),
|
||||
),
|
||||
);
|
||||
return LayerZeroDvn(
|
||||
dvns: parsedDvns,
|
||||
status: json['status'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroDvnStatus {
|
||||
final String? status;
|
||||
final String? txHash;
|
||||
final String? blockHash;
|
||||
final int? blockNumber;
|
||||
final int? blockTimestamp;
|
||||
|
||||
LayerZeroDvnStatus({
|
||||
this.status,
|
||||
this.txHash,
|
||||
this.blockHash,
|
||||
this.blockNumber,
|
||||
this.blockTimestamp,
|
||||
});
|
||||
|
||||
factory LayerZeroDvnStatus.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroDvnStatus(
|
||||
status: json['status'] as String?,
|
||||
txHash: json['txHash'] as String?,
|
||||
blockHash: json['blockHash'] as String?,
|
||||
blockNumber: json['blockNumber'] as int?,
|
||||
blockTimestamp: json['blockTimestamp'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LayerZeroSealer {
|
||||
final String? status;
|
||||
final LayerZeroTransaction? tx;
|
||||
|
||||
LayerZeroSealer({
|
||||
this.status,
|
||||
this.tx,
|
||||
});
|
||||
|
||||
factory LayerZeroSealer.fromJson(Map<String, dynamic> json) {
|
||||
return LayerZeroSealer(
|
||||
status: json['status'] as String?,
|
||||
tx: json['tx'] != null
|
||||
? LayerZeroTransaction.fromJson(
|
||||
json['tx'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
+45
-1
@@ -32,6 +32,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';
|
||||
import 'package:cake_wallet/core/yat_service.dart';
|
||||
import 'package:cake_wallet/decred/decred.dart';
|
||||
import 'package:cake_wallet/entities/biometric_auth.dart';
|
||||
import 'package:cake_wallet/entities/bridge_transfer.dart';
|
||||
import 'package:cake_wallet/entities/contact.dart';
|
||||
import 'package:cake_wallet/entities/contact_record.dart';
|
||||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
@@ -52,6 +53,9 @@ import 'package:cake_wallet/nano/nano.dart';
|
||||
import 'package:cake_wallet/new-ui/new_dashboard.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/about_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/home_page.dart';
|
||||
@@ -163,6 +167,8 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_details_
|
||||
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
|
||||
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
|
||||
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
|
||||
@@ -183,6 +189,7 @@ import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/orders_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
|
||||
import 'package:cake_wallet/store/bridge_transfers_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
|
||||
import 'package:cake_wallet/store/node_list_store.dart';
|
||||
@@ -202,6 +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/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';
|
||||
@@ -269,6 +278,7 @@ import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
|
||||
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
@@ -327,7 +337,6 @@ late Box<Order> _ordersSource;
|
||||
late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
|
||||
late Box<PayjoinSession> _payjoinSessionSource;
|
||||
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
|
||||
|
||||
Future<void> setup({
|
||||
required Box<Node> nodeSource,
|
||||
required Box<Node> powNodeSource,
|
||||
@@ -396,6 +405,7 @@ Future<void> setup({
|
||||
TradesStore(tradesSource: _tradesSource, appStore: getIt.get<AppStore>()));
|
||||
getIt.registerSingleton<OrdersStore>(
|
||||
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
|
||||
getIt.registerSingleton<BridgeTransfersStore>(BridgeTransfersStore());
|
||||
getIt.registerFactory(() =>
|
||||
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
|
||||
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
|
||||
@@ -1761,11 +1771,45 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => DEuroSavingsPage(getIt<DEuroViewModel>()));
|
||||
|
||||
getIt.registerFactory(() => BridgeViewModel(
|
||||
appStore: getIt.get<AppStore>(),
|
||||
bridgeTransfersStore: getIt.get<BridgeTransfersStore>(),
|
||||
walletManager: getIt.get<WalletManager>(),
|
||||
fiatConversionStore: getIt.get<FiatConversionStore>(),
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
));
|
||||
|
||||
getIt.registerFactory(() => BridgeHistoryViewModel(
|
||||
bridgeTransfersStore: getIt.get<BridgeTransfersStore>(),
|
||||
appStore: getIt.get<AppStore>(),
|
||||
));
|
||||
getIt.registerFactoryParam<BridgeDetailsViewModel, BridgeTransfer, void>(
|
||||
(BridgeTransfer transfer, _) {
|
||||
final appStore = getIt.get<AppStore>();
|
||||
return BridgeDetailsViewModel(
|
||||
transferForDetails: transfer,
|
||||
bridgeTransfersStore: getIt.get<BridgeTransfersStore>(),
|
||||
walletId: appStore.wallet?.name ?? transfer.walletId,
|
||||
);
|
||||
});
|
||||
getIt.registerFactoryParam<BridgeDetailPage, BridgeTransfer, void>(
|
||||
(BridgeTransfer transfer, _) => BridgeDetailPage(
|
||||
viewModel: getIt.get<BridgeDetailsViewModel>(param1: transfer),
|
||||
));
|
||||
|
||||
getIt.registerLazySingleton(() => NodeSwitchingService(
|
||||
appStore: getIt.get<AppStore>(),
|
||||
settingsStore: getIt.get<SettingsStore>(),
|
||||
nodeSource: _nodeSource,
|
||||
));
|
||||
|
||||
getIt.registerFactoryParam<BridgeAmountPage, CryptoCurrency, void>(
|
||||
(CryptoCurrency initialToken, _) => BridgeAmountPage(
|
||||
bridgeViewModel: getIt.get<BridgeViewModel>(),
|
||||
bridgeHistoryViewModel: getIt.get<BridgeHistoryViewModel>(),
|
||||
initialToken: initialToken,
|
||||
),
|
||||
);
|
||||
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'package:cw_core/db/sqlite.dart';
|
||||
|
||||
class BridgeTransfer {
|
||||
BridgeTransfer({
|
||||
required this.id,
|
||||
required this.walletId,
|
||||
required this.sourceChainId,
|
||||
required this.destinationChainId,
|
||||
required this.tokenSymbol,
|
||||
required this.tokenContract,
|
||||
required this.amount,
|
||||
required this.recipientAddress,
|
||||
required this.sourceTxHash,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
this.confirmedAt,
|
||||
this.amountRaw,
|
||||
this.errorMessage,
|
||||
this.statusMessage,
|
||||
});
|
||||
|
||||
static const tableName = 'BridgeTransfer';
|
||||
|
||||
static Future<List<BridgeTransfer>> selectAll() async {
|
||||
final database = db;
|
||||
if (database == null) return [];
|
||||
|
||||
final rows = await database.query(
|
||||
tableName,
|
||||
orderBy: 'created_at DESC',
|
||||
);
|
||||
return rows.map(fromRow).toList();
|
||||
}
|
||||
|
||||
static Future<void> insert(BridgeTransfer transfer) async {
|
||||
final database = db;
|
||||
if (database == null) return;
|
||||
|
||||
await database.insert(tableName, transfer.toRow());
|
||||
}
|
||||
|
||||
static Future<void> update(BridgeTransfer transfer) async {
|
||||
final database = db;
|
||||
if (database == null) return;
|
||||
|
||||
await database.update(
|
||||
tableName,
|
||||
transfer.toRow(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [transfer.id],
|
||||
);
|
||||
}
|
||||
|
||||
String id;
|
||||
String walletId;
|
||||
int sourceChainId;
|
||||
int destinationChainId;
|
||||
String tokenSymbol;
|
||||
String tokenContract;
|
||||
String amount;
|
||||
String recipientAddress;
|
||||
String sourceTxHash;
|
||||
String status;
|
||||
DateTime createdAt;
|
||||
DateTime? updatedAt;
|
||||
DateTime? confirmedAt;
|
||||
String? amountRaw;
|
||||
String? errorMessage;
|
||||
String? statusMessage;
|
||||
|
||||
bool get isActive => status == 'submitted' || status == 'confirming' || status == 'initiated';
|
||||
|
||||
Map<String, Object?> toRow() {
|
||||
return {
|
||||
'id': id,
|
||||
'wallet_id': walletId,
|
||||
'source_chain_id': sourceChainId,
|
||||
'destination_chain_id': destinationChainId,
|
||||
'token_symbol': tokenSymbol,
|
||||
'token_contract': tokenContract,
|
||||
'amount': amount,
|
||||
'recipient_address': recipientAddress,
|
||||
'source_tx_hash': sourceTxHash,
|
||||
'status': status,
|
||||
'created_at': createdAt.millisecondsSinceEpoch,
|
||||
'updated_at': updatedAt?.millisecondsSinceEpoch,
|
||||
'confirmed_at': confirmedAt?.millisecondsSinceEpoch,
|
||||
'amount_raw': amountRaw,
|
||||
'error_message': errorMessage,
|
||||
'status_message': statusMessage,
|
||||
};
|
||||
}
|
||||
|
||||
static int? _nullableInt(Object? v) {
|
||||
if (v == null) return null;
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
return int.tryParse(v.toString());
|
||||
}
|
||||
|
||||
static int _parseInt(Object? v) => _nullableInt(v) ?? 0;
|
||||
static DateTime _parseDateTime(Object? v) => DateTime.fromMillisecondsSinceEpoch(_parseInt(v));
|
||||
|
||||
static BridgeTransfer fromRow(Map<String, Object?> m) {
|
||||
String? asStr(Object? v) => v as String?;
|
||||
|
||||
return BridgeTransfer(
|
||||
id: m['id'] as String,
|
||||
walletId: m['wallet_id'] as String,
|
||||
sourceChainId: _parseInt(m['source_chain_id']),
|
||||
destinationChainId: _parseInt(m['destination_chain_id']),
|
||||
tokenSymbol: m['token_symbol'] as String,
|
||||
tokenContract: m['token_contract'] as String,
|
||||
amount: m['amount'] as String,
|
||||
recipientAddress: m['recipient_address'] as String,
|
||||
sourceTxHash: m['source_tx_hash'] as String,
|
||||
status: m['status'] as String,
|
||||
createdAt: _parseDateTime(m['created_at']),
|
||||
updatedAt: _parseDateTime(m['updated_at']),
|
||||
confirmedAt: _parseDateTime(m['confirmed_at']),
|
||||
amountRaw: asStr(m['amount_raw']),
|
||||
errorMessage: asStr(m['error_message']),
|
||||
statusMessage: asStr(m['status_message']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'package:cake_wallet/entities/hash_wallet_identifier.dart';
|
||||
import 'package:cake_wallet/entities/wallet_group.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WalletManager {
|
||||
@@ -163,4 +162,14 @@ class WalletManager {
|
||||
await _sharedPreferences.remove(oldNameKey);
|
||||
}
|
||||
}
|
||||
|
||||
String? getGroupName(WalletInfo walletInfo) {
|
||||
try {
|
||||
final groupKey = _resolveGroupKey(walletInfo);
|
||||
final group = walletGroups.firstWhere((g) => g.groupKey == groupKey);
|
||||
return group.groupName;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +226,22 @@ class CWEVM extends EVM {
|
||||
@override
|
||||
Web3Client? getWeb3Client(WalletBase wallet) => (wallet as EVMChainWallet).getWeb3Client();
|
||||
|
||||
@override
|
||||
Future<bool?> getTransactionReceipt(WalletBase wallet, String txHash) async {
|
||||
final client = getWeb3Client(wallet);
|
||||
if (client == null) return null;
|
||||
|
||||
try {
|
||||
final receipt = await client.getTransactionReceipt(txHash);
|
||||
|
||||
if (receipt == null) return null;
|
||||
|
||||
return receipt.status;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String getTokenAddress(CryptoCurrency asset) => (asset as Erc20Token).contractAddress;
|
||||
|
||||
@@ -492,10 +508,24 @@ class CWEVM extends EVM {
|
||||
chainId: config.chainId,
|
||||
name: config.name,
|
||||
shortCode: config.shortCode,
|
||||
currency: config.nativeCurrency,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
ChainInfo? getChainInfoByChainId(int chainId) {
|
||||
final config = _registry.getChainConfig(chainId);
|
||||
if (config == null) return null;
|
||||
|
||||
return ChainInfo(
|
||||
chainId: config.chainId,
|
||||
name: config.name,
|
||||
shortCode: config.shortCode,
|
||||
currency: config.nativeCurrency,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ChainInfo? getCurrentChain(WalletBase wallet) {
|
||||
if (wallet is EVMChainWallet) {
|
||||
@@ -505,6 +535,7 @@ class CWEVM extends EVM {
|
||||
chainId: config.chainId,
|
||||
name: config.name,
|
||||
shortCode: config.shortCode,
|
||||
currency: config.nativeCurrency,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -542,6 +573,85 @@ class CWEVM extends EVM {
|
||||
bool hasPriorityFee(int chainId) => EVMChainUtils.hasPriorityFee(chainId);
|
||||
|
||||
@override
|
||||
bool isUSDT0Token(WalletBase wallet, CryptoCurrency token) {
|
||||
if (token is! Erc20Token) return false;
|
||||
|
||||
final chainId = getSelectedChainId(wallet);
|
||||
if (chainId == null) return false;
|
||||
|
||||
return USDT0Config.isUSDT0Token(token, chainId);
|
||||
}
|
||||
|
||||
@override
|
||||
List<ChainInfo> getUSDT0DestinationChains(WalletBase wallet) {
|
||||
final currentChainId = getSelectedChainId(wallet);
|
||||
if (currentChainId == null) return [];
|
||||
|
||||
final result = <ChainInfo>[];
|
||||
for (final config in _registry.getAllChains()) {
|
||||
if (USDT0Config.isChainSupported(config.chainId) && config.chainId != currentChainId) {
|
||||
result.add(ChainInfo(
|
||||
chainId: config.chainId,
|
||||
name: config.name,
|
||||
shortCode: config.shortCode,
|
||||
currency: config.nativeCurrency,
|
||||
));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<USDT0Quote> quoteUSDT0Transfer({
|
||||
required WalletBase wallet,
|
||||
required int sourceChainId,
|
||||
required int destinationChainId,
|
||||
required BigInt amount,
|
||||
required String recipientAddress,
|
||||
}) {
|
||||
final evmWallet = wallet as EVMChainWallet;
|
||||
final client = evmWallet.getWeb3Client();
|
||||
if (client == null) {
|
||||
throw StateError('Wallet not connected');
|
||||
}
|
||||
|
||||
return USDT0Service.quoteCrossChainTransfer(
|
||||
client: client,
|
||||
sourceChainId: sourceChainId,
|
||||
destinationChainId: destinationChainId,
|
||||
amount: amount,
|
||||
recipientAddress: recipientAddress,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingTransaction> executeUSDT0Transfer({
|
||||
required WalletBase wallet,
|
||||
required CryptoCurrency token,
|
||||
required int sourceChainId,
|
||||
required int destinationChainId,
|
||||
required BigInt amount,
|
||||
required String recipientAddress,
|
||||
required USDT0Quote quote,
|
||||
required TransactionPriority priority,
|
||||
bool useBlinkProtection = true,
|
||||
}) {
|
||||
final evmWallet = wallet as EVMChainWallet;
|
||||
final tokenErc20 = token as Erc20Token;
|
||||
|
||||
return USDT0Service.executeCrossChainTransfer(
|
||||
wallet: evmWallet,
|
||||
sourceChainId: sourceChainId,
|
||||
destinationChainId: destinationChainId,
|
||||
amount: amount,
|
||||
recipientAddress: recipientAddress,
|
||||
quote: quote,
|
||||
token: tokenErc20,
|
||||
priority: priority as EVMChainTransactionPriority,
|
||||
useBlinkProtection: useBlinkProtection,
|
||||
|
||||
);
|
||||
}
|
||||
Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
|
||||
WalletBase wallet,
|
||||
TransactionPriority priority,
|
||||
|
||||
+1
-1
@@ -362,10 +362,10 @@ Future<void> initialSetup({
|
||||
powNodeSource: powNodes,
|
||||
contactSource: contactSource,
|
||||
tradesSource: tradesSource,
|
||||
ordersSource: ordersSource,
|
||||
templates: templates,
|
||||
exchangeTemplates: exchangeTemplates,
|
||||
transactionDescriptionBox: transactionDescriptions,
|
||||
ordersSource: ordersSource,
|
||||
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
|
||||
unspentCoinsInfoSource: unspentCoinsInfoSource,
|
||||
payjoinSessionSource: payjoinSessionSource,
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/utils/request_review_handler.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.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_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class BridgeAmountPage extends StatefulWidget {
|
||||
const BridgeAmountPage({
|
||||
super.key,
|
||||
required this.bridgeViewModel,
|
||||
required this.bridgeHistoryViewModel,
|
||||
required this.initialToken,
|
||||
});
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
final BridgeHistoryViewModel bridgeHistoryViewModel;
|
||||
|
||||
final CryptoCurrency initialToken;
|
||||
|
||||
@override
|
||||
State<BridgeAmountPage> createState() => _BridgeAmountPageState();
|
||||
}
|
||||
|
||||
class _BridgeAmountPageState extends State<BridgeAmountPage> {
|
||||
late final TextEditingController _amountController;
|
||||
BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bridgeViewModel.applyInitialBridgeToken(widget.initialToken);
|
||||
bridgeViewModel.onBridgeSuccess = _showBridgeSuccessBottomSheet;
|
||||
|
||||
_amountController = TextEditingController();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
bridgeViewModel.ensureFiatPriceForSelectedToken();
|
||||
});
|
||||
}
|
||||
|
||||
void _showBridgeSuccessBottomSheet() {
|
||||
if (!mounted) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
final ctx = context;
|
||||
if (!ctx.mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: ctx,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return InfoBottomSheet(
|
||||
footerType: FooterType.doubleActionButton,
|
||||
titleText: 'Bridge initiated!',
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
content: 'The bridging will take between 30 seconds and 3 '
|
||||
'minutes to complete.',
|
||||
doubleActionLeftButtonText: S.of(bottomSheetContext).close,
|
||||
doubleActionRightButtonText: 'View history',
|
||||
onLeftActionButtonPressed: () {
|
||||
bridgeViewModel.clearBridgeSuccess();
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
RequestReviewHandler.requestReview();
|
||||
},
|
||||
onRightActionButtonPressed: () {
|
||||
bridgeViewModel.clearBridgeSuccess();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => BridgeHistoryPage(widget.bridgeHistoryViewModel),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bridgeViewModel.onBridgeSuccess = null;
|
||||
bridgeViewModel.setAmount('');
|
||||
_amountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return KeyboardHideOverlay(
|
||||
unfocusOnTap: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: 'Enter Amount',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
|
||||
trailingIcon: const Icon(Icons.calendar_month, size: 18),
|
||||
onTrailingPressed: () {
|
||||
Navigator.pushNamed(context, Routes.bridgeHistoryPage, arguments: widget.bridgeHistoryViewModel);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final canNext = bridgeViewModel.canProceedToDestinationNetwork;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Spacer(flex: 2),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: (constraints.maxWidth - 200)
|
||||
.clamp(40.0, constraints.maxWidth),
|
||||
minWidth: 40,
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _amountController,
|
||||
maxLines: 1,
|
||||
onChanged: bridgeViewModel.setAmount,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
validator: bridgeViewModel.decimalAmountValidator,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
signed: false,
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d*[.,]?\d*$'),
|
||||
),
|
||||
],
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
hintText: '0.00',
|
||||
hintStyle: theme.textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 45,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
bridgeViewModel.selectedToken?.title ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.displayMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 45,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Text(
|
||||
bridgeViewModel.fiatAmountFormatted.isEmpty
|
||||
? ''
|
||||
: '~${bridgeViewModel.fiatAmountFormatted} ${bridgeViewModel.fiatCurrencyTitle}',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 22,
|
||||
letterSpacing: -0.11,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bridgeViewModel.amountError != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
bridgeViewModel.amountError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(flex: 3),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: RichText(
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Available ',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${bridgeViewModel.tokenBalanceFormatted} '
|
||||
'${bridgeViewModel.selectedToken?.title ?? ''}',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
backgroundColor: theme.colorScheme.surfaceContainer,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
bridgeViewModel.setMaxAmount();
|
||||
_amountController.text = bridgeViewModel.amount;
|
||||
_amountController.selection = TextSelection.collapsed(
|
||||
offset: _amountController.text.length,
|
||||
);
|
||||
},
|
||||
child: Text(S.of(context).max),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IgnorePointer(
|
||||
ignoring: !canNext,
|
||||
child: ModernButton(
|
||||
size: 48,
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
iconColor: theme.colorScheme.onPrimary,
|
||||
icon: Icon(Icons.arrow_forward, size: 25),
|
||||
onPressed: () {
|
||||
_amountController.clear();
|
||||
Navigator.pushNamed(
|
||||
context, Routes.bridgeDestinationNetworkPage, arguments: bridgeViewModel);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/bridge/confirm_details_card.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/bridge/network_path_pill.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class BridgeConfirmSheet extends StatefulWidget {
|
||||
const BridgeConfirmSheet(this.bridgeViewModel);
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
|
||||
@override
|
||||
State<BridgeConfirmSheet> createState() => _BridgeConfirmSheetState();
|
||||
}
|
||||
|
||||
class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
|
||||
late final ReactionDisposer _successDisposer;
|
||||
|
||||
BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_successDisposer = reaction(
|
||||
(_) => bridgeViewModel.bridgeSuccess,
|
||||
(bool success) {
|
||||
if (success && mounted) {
|
||||
Navigator.of(context).maybePop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
bridgeViewModel.loadQuote();
|
||||
bridgeViewModel.ensureFiatPriceForSelectedToken();
|
||||
bridgeViewModel.ensureFiatPriceForNativeCurrency();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_successDisposer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return ModalTopBar(
|
||||
title: '',
|
||||
leadingWidget: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: bridgeViewModel.selectedToken?.iconPath ?? '',
|
||||
width: 36,
|
||||
height: 36,
|
||||
),
|
||||
Text(
|
||||
'Bridge',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 20,
|
||||
letterSpacing: -0.1,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailingIcon: const Icon(Icons.close),
|
||||
onTrailingPressed: () => Navigator.of(context).maybePop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
if (bridgeViewModel.isQuoteLoading && bridgeViewModel.quote == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 48),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (bridgeViewModel.quoteError != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
bridgeViewModel.quoteError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.07,
|
||||
color: scheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () => bridgeViewModel.loadQuote(),
|
||||
child: Text(
|
||||
S.of(context).try_again,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.08,
|
||||
color: scheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 24,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Text(
|
||||
bridgeViewModel.amountDisplayFormatted,
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
bridgeViewModel.selectedToken?.title ?? '',
|
||||
style: Theme.of(context).textTheme.displaySmall?.copyWith(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${bridgeViewModel.fiatCurrencyTitle} ${bridgeViewModel.fiatAmountFormatted}',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: scheme.onSurfaceVariant,
|
||||
letterSpacing: -0.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
NetworkPathPill(
|
||||
sourceChainName: bridgeViewModel.wallet.type.name,
|
||||
destChainName: bridgeViewModel.destinationChainInfo?.name ?? '',
|
||||
),
|
||||
ConfirmDetailsCard(bridgeViewModel: bridgeViewModel),
|
||||
if (bridgeViewModel.executeError != null) ...[
|
||||
Text(
|
||||
bridgeViewModel.executeError!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: scheme.error),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
if (bridgeViewModel.isExecuting) {
|
||||
return const LoadingBottomWidget(
|
||||
text: 'Bridging...',
|
||||
);
|
||||
}
|
||||
final canSwipe = bridgeViewModel.quote != null && !bridgeViewModel.isQuoteLoading;
|
||||
return IgnorePointer(
|
||||
ignoring: !canSwipe,
|
||||
child: Opacity(
|
||||
opacity: canSwipe ? 1 : 0.45,
|
||||
child: ConfirmSwiper(
|
||||
onConfirmed: () {
|
||||
bridgeViewModel.executeBridge();
|
||||
},
|
||||
swiperText: 'Swipe to bridge',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
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:cw_core/generate_name.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class BridgeDetailPage extends StatelessWidget {
|
||||
BridgeDetailPage({super.key, required this.viewModel});
|
||||
|
||||
final BridgeDetailsViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.25,
|
||||
maxChildSize: 0.9,
|
||||
snap: true,
|
||||
snapSizes: const [0.9],
|
||||
builder: (context, controller) {
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "Bridge Transfer Details",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
NewListSections(
|
||||
sections: {
|
||||
"": viewModel.items
|
||||
.where((item) => item is! TrackTradeListItem)
|
||||
.map((item) {
|
||||
return ListItemRegularRow(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: item.value));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
showArrow: false,
|
||||
keyValue: item.title,
|
||||
label: item.title,
|
||||
trailingWidget: _buildTrailingWidget(item, context),
|
||||
bottomWidget: _buildBottomWidget(item, context),
|
||||
);
|
||||
}).toList(),
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
NewListSections(sections: {
|
||||
"": viewModel.items
|
||||
.where((item) => item is TrackTradeListItem)
|
||||
.map((item) {
|
||||
return ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label: item.title,
|
||||
onTap: () => (item as TrackTradeListItem).onTap(),
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8,
|
||||
);
|
||||
}).toList()
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomWidget(TransactionDetailsListItem item, BuildContext context) {
|
||||
if (item is AddressListItem) {
|
||||
return AddressFormatter.buildSegmentedAddress(
|
||||
address: item.value,
|
||||
evenTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface));
|
||||
}
|
||||
|
||||
final isCompleted = item.value.contains("Completed");
|
||||
final isInitiated = item.value.contains("initiated");
|
||||
if (isCompleted || isInitiated) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: isCompleted ? CustomThemeColors.syncGreen : CustomThemeColors.syncYellow,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
item.value,
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildTrailingWidget(TransactionDetailsListItem item, BuildContext context) {
|
||||
final isCompleted = item.value.contains("Completed");
|
||||
final isInitiated = item.value.contains("initiated");
|
||||
if (item is AddressListItem || isCompleted || isInitiated) {
|
||||
return SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Text(
|
||||
item.value.capitalized(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontFamily: 'Wix Madefor Text',
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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/new-ui/widgets/bridge/transfer_history_row.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class BridgeHistoryPage extends StatelessWidget {
|
||||
BridgeHistoryPage(this.viewModel);
|
||||
|
||||
final BridgeHistoryViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: "Bridge history",
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
if (viewModel.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"No bridge transfers yet.",
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final active = viewModel.activeTransfers;
|
||||
final past = viewModel.pastTransfers;
|
||||
final items = <Widget>[];
|
||||
|
||||
if (active.isNotEmpty) {
|
||||
items.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"In progress",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final transfer in active) {
|
||||
items.add(
|
||||
TransferHistoryRow(
|
||||
transfer: transfer,
|
||||
onTap: () {
|
||||
final page = getIt.get<BridgeDetailPage>(param1: transfer);
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => page,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (past.isNotEmpty) {
|
||||
items.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0, top: 16),
|
||||
child: Text(
|
||||
"Past",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
),
|
||||
);
|
||||
for (final transfer in past) {
|
||||
items.add(
|
||||
TransferHistoryRow(
|
||||
transfer: transfer,
|
||||
onTap: () {
|
||||
final page = getIt.get<BridgeDetailPage>(param1: transfer);
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => page,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8),
|
||||
children: items,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/list_item_regular_row_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class BridgeNetworkPage extends StatelessWidget {
|
||||
const BridgeNetworkPage(this.bridgeViewModel);
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return KeyboardHideOverlay(
|
||||
unfocusOnTap: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: 'Destination Network',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 8, 18, 16),
|
||||
child: Text(
|
||||
'Select what Network to transfer your assets to',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return NewListSections(
|
||||
sections: {
|
||||
'': bridgeViewModel.availableDestinationChains.map(
|
||||
(chain) {
|
||||
final chainName = chain.name;
|
||||
return ListItemRegularRow(
|
||||
iconPath: 'assets/images/crypto/${chainName.toLowerCase()}.webp',
|
||||
keyValue: chain.chainId.toString(),
|
||||
label: chainName,
|
||||
onTap: () {
|
||||
bridgeViewModel.setDestinationChain(chain.chainId);
|
||||
Navigator.pushNamed(context, Routes.bridgeReceivingWalletPage, arguments: bridgeViewModel);
|
||||
},
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final src = bridgeViewModel.wallet.type.name;
|
||||
|
||||
return ListItemRegularRowWidget(
|
||||
isFirstInSection: true,
|
||||
isLastInSection: true,
|
||||
keyValue: 'sending_from',
|
||||
label: 'Sending from',
|
||||
trailingIconPath: 'assets/new-ui/chain_badges/${src.toLowerCase()}.svg',
|
||||
trailingText: src.capitalized(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class BridgeReceiveAddressInputPage extends StatefulWidget {
|
||||
const BridgeReceiveAddressInputPage({super.key, required this.bridgeViewModel});
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
|
||||
@override
|
||||
State<BridgeReceiveAddressInputPage> createState() => _BridgeReceiveAddressInputPageState();
|
||||
}
|
||||
|
||||
class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInputPage> {
|
||||
BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
|
||||
|
||||
late final TextEditingController _controller;
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
_controller.addListener(() => setState(() {}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return KeyboardHideOverlay(
|
||||
unfocusOnTap: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: 'Input Receive Address',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final chainName = bridgeViewModel.destinationChainInfo?.name;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Make sure it is compatible with:',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl:
|
||||
'assets/new-ui/chain_badges/${chainName?.toLowerCase()}.svg',
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
chainName ?? '',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
NewSendAddressInput(
|
||||
hintText: 'Enter Address',
|
||||
addressController: _controller,
|
||||
focusNode: _focusNode,
|
||||
selectedCurrency: bridgeViewModel.wallet.currency,
|
||||
onEditingComplete: () {},
|
||||
validator: AddressValidator(type: bridgeViewModel.destinationChainInfo!.currency),
|
||||
),
|
||||
const Spacer(),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return FilledButton(
|
||||
onPressed: () {
|
||||
final overlayCtx = Navigator.of(context).overlay?.context;
|
||||
bridgeViewModel.setRecipientAddress(_controller.text.trim());
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (overlayCtx != null && overlayCtx.mounted) {
|
||||
showMaterialModalBottomSheet<void>(
|
||||
context: overlayCtx,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) =>
|
||||
BridgeConfirmSheet(bridgeViewModel),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
),
|
||||
child: Text(S.of(context).continue_text),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/list_Item_style_wrapper.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class BridgeReceivingWalletPage extends StatefulWidget {
|
||||
const BridgeReceivingWalletPage(this.bridgeViewModel);
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
|
||||
@override
|
||||
State<BridgeReceivingWalletPage> createState() => _BridgeReceivingWalletPageState();
|
||||
}
|
||||
|
||||
class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
|
||||
BridgeViewModel get bridgeViewModel => widget.bridgeViewModel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bridgeViewModel.loadReceivingWalletOptions();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeyboardHideOverlay(
|
||||
unfocusOnTap: true,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: 'Receiving Wallet',
|
||||
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
|
||||
onLeadingPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final chain = bridgeViewModel.destinationChainInfo;
|
||||
|
||||
final chainName = chain?.name ?? '';
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: CakeImageWidget(
|
||||
borderRadius: 8,
|
||||
imageUrl: chainName.isNotEmpty
|
||||
? 'assets/images/crypto/${chainName.toLowerCase()}.webp'
|
||||
: null,
|
||||
width: 72,
|
||||
height: 72,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Select a $chainName wallet or address to send '
|
||||
'the bridged USDT0 to.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.07,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (bridgeViewModel.isBridgeReceivingWalletListLoading)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: _receivingListRows(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _receivingListRows(BuildContext context) {
|
||||
final options = bridgeViewModel.bridgeReceivingWalletOptions;
|
||||
final rows = <Widget>[];
|
||||
|
||||
for (int i = 0; i < options.length; i++) {
|
||||
final option = options[i];
|
||||
|
||||
if (i > 0) rows.add(const SizedBox(height: 12));
|
||||
|
||||
rows.add(
|
||||
_BridgeReceivingWalletRow(
|
||||
option: option,
|
||||
onTap: () {
|
||||
bridgeViewModel.setRecipientAddress(option.address, destWalletName: option.name);
|
||||
|
||||
showMaterialModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => BridgeConfirmSheet(bridgeViewModel),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.isNotEmpty) rows.add(const SizedBox(height: 12));
|
||||
|
||||
rows.add(
|
||||
ListItemStyleWrapper(
|
||||
isFirstInSection: true,
|
||||
isLastInSection: true,
|
||||
height: 48,
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
onTap: () {
|
||||
showMaterialModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) => BridgeReceiveAddressInputPage(bridgeViewModel: bridgeViewModel),
|
||||
);
|
||||
},
|
||||
builder: (ctx, textStyle, _) {
|
||||
final scheme = Theme.of(ctx).colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 24,
|
||||
color: scheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Input an Address',
|
||||
style: textStyle.copyWith(
|
||||
color: scheme.primary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: -0.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
class _BridgeReceivingWalletRow extends StatelessWidget {
|
||||
const _BridgeReceivingWalletRow({
|
||||
required this.option,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final BridgeReceivingWalletOption option;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasGroupRow = option.groupLabel != null;
|
||||
|
||||
return ListItemStyleWrapper(
|
||||
onTap: onTap,
|
||||
isFirstInSection: true,
|
||||
isLastInSection: true,
|
||||
height: hasGroupRow ? 62.0 : 48.0,
|
||||
builder: (ctx, textStyle, labelStyle) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.name,
|
||||
style: textStyle.copyWith(letterSpacing: -0.07),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (hasGroupRow) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_balance_wallet_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
option.groupLabel!,
|
||||
style: labelStyle.copyWith(
|
||||
fontSize: 12,
|
||||
letterSpacing: -0.06,
|
||||
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (option.isCurrent) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Current',
|
||||
style: textStyle.copyWith(
|
||||
fontSize: 12,
|
||||
color: Theme.of(ctx).colorScheme.primary,
|
||||
letterSpacing: -0.06,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/arrow_forward.svg',
|
||||
height: 16,
|
||||
color: Theme.of(ctx).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/bridge/expandable_details_card.dart';
|
||||
import 'package:cake_wallet/view_model/bridge/bridge_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class ConfirmDetailsCard extends StatelessWidget {
|
||||
const ConfirmDetailsCard({required this.bridgeViewModel});
|
||||
|
||||
final BridgeViewModel bridgeViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final feeParts = <String>[
|
||||
bridgeViewModel.quoteNativeFeeFormattedForDisplay,
|
||||
bridgeViewModel.quoteNativeFiatFeeFormattedForDisplay,
|
||||
];
|
||||
final feeLine = feeParts.join(' ');
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ExpandableBridgeDetailRow(
|
||||
label: S.of(context).from,
|
||||
title: bridgeViewModel.wallet.name,
|
||||
address: bridgeViewModel.sourceAddress,
|
||||
showChevron: true,
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: scheme.surfaceContainerHigh,
|
||||
),
|
||||
ExpandableBridgeDetailRow(
|
||||
label: S.of(context).to,
|
||||
title: bridgeViewModel.destinationWalletName ?? bridgeViewModel.recipientAddress.trim(),
|
||||
address: bridgeViewModel.recipientAddress.trim(),
|
||||
showChevron: bridgeViewModel.destinationWalletName != null,
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
color: scheme.surfaceContainerHigh,
|
||||
),
|
||||
ExpandableBridgeDetailRow(
|
||||
label: S.of(context).fee,
|
||||
title: feeLine,
|
||||
showChevron: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExpandableBridgeDetailRow extends StatefulWidget {
|
||||
const ExpandableBridgeDetailRow({
|
||||
required this.label,
|
||||
required this.title,
|
||||
this.address,
|
||||
this.showChevron = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String title;
|
||||
final String? address;
|
||||
final bool showChevron;
|
||||
|
||||
@override
|
||||
State<ExpandableBridgeDetailRow> createState() => ExpandableBridgeDetailRowState();
|
||||
}
|
||||
|
||||
class ExpandableBridgeDetailRowState extends State<ExpandableBridgeDetailRow> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final showChevron = widget.showChevron;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: showChevron ? () => setState(() => _expanded = !_expanded) : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.07,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.title,
|
||||
textAlign: TextAlign.end,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
letterSpacing: -0.07,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showChevron) ...[
|
||||
const SizedBox(width: 2),
|
||||
AnimatedRotation(
|
||||
turns: _expanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 22,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: showChevron && _expanded
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SelectableText(
|
||||
widget.address ?? '',
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
height: 1.35,
|
||||
color: scheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class NetworkPathPill extends StatelessWidget {
|
||||
const NetworkPathPill({
|
||||
required this.sourceChainName,
|
||||
required this.destChainName,
|
||||
this.showBackground = true,
|
||||
});
|
||||
|
||||
final String sourceChainName;
|
||||
final String destChainName;
|
||||
final bool showBackground;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final body = Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/chain_badges/${sourceChainName.toLowerCase()}.svg',
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
sourceChainName.capitalized(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Icon(
|
||||
Icons.arrow_forward,
|
||||
size: 16,
|
||||
color: scheme.primary,
|
||||
),
|
||||
),
|
||||
CakeImageWidget(
|
||||
imageUrl: 'assets/new-ui/chain_badges/${destChainName.toLowerCase()}.svg',
|
||||
width: 24,
|
||||
height: 24,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(
|
||||
child: Text(
|
||||
destChainName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: scheme.onSurface,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 16,
|
||||
letterSpacing: -0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
return showBackground
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(80),
|
||||
),
|
||||
child: body,
|
||||
)
|
||||
: body;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import 'package:cake_wallet/entities/bridge_transfer.dart';
|
||||
import 'package:cake_wallet/evm/evm.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class TransferHistoryRow extends StatelessWidget {
|
||||
const TransferHistoryRow({
|
||||
required this.transfer,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BridgeTransfer transfer;
|
||||
final VoidCallback onTap;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Color _statusColor(BuildContext context, String status) {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return CustomThemeColors.syncGreen;
|
||||
case 'failed':
|
||||
return Theme.of(context).colorScheme.error;
|
||||
case 'submitted':
|
||||
case 'confirming':
|
||||
case 'initiated':
|
||||
default:
|
||||
return CustomThemeColors.syncYellow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sourceChainName = evm?.getChainInfoByChainId(transfer.sourceChainId)?.name;
|
||||
final destChainName = evm?.getChainInfoByChainId(transfer.destinationChainId)?.name;
|
||||
final formattedDate = DateFormat('HH:mm').format(transfer.createdAt);
|
||||
final statusText = _statusLabel(transfer.status);
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: double.infinity,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
Theme.of(context).colorScheme.surfaceContainer,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 42,
|
||||
width: 42,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${sourceChainName?.toLowerCase()}.webp',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
Positioned(
|
||||
top: 14,
|
||||
left: 12,
|
||||
child: CakeImageWidget(
|
||||
imageUrl: 'assets/images/crypto/${destChainName?.toLowerCase()}.webp',
|
||||
width: 28,
|
||||
height: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'$sourceChainName → $destChainName',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${transfer.amount} ${transfer.tokenSymbol}',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 8,
|
||||
width: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _statusColor(context, transfer.status),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
statusText,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/modal_navigator.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/send_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
|
||||
@@ -31,6 +32,7 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
required this.mode,
|
||||
required this.wallet,
|
||||
required this.showSwap,
|
||||
required this.showBridgeButton,
|
||||
this.asset});
|
||||
|
||||
final String title;
|
||||
@@ -44,6 +46,7 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
final String chainIconPath;
|
||||
final WalletBase wallet;
|
||||
final bool showSwap;
|
||||
final bool showBridgeButton;
|
||||
final AssetDetailsModalModes mode;
|
||||
|
||||
@override
|
||||
@@ -59,7 +62,7 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
title: "",
|
||||
trailingIcon: Icon(Icons.close),
|
||||
onTrailingPressed: Navigator.of(context).pop,
|
||||
padding: EdgeInsets.only(top:12,right:18),
|
||||
padding: EdgeInsets.only(top: 12, right: 18),
|
||||
),
|
||||
SafeArea(
|
||||
child: Column(
|
||||
@@ -125,21 +128,20 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
if(asset != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||
|
||||
child: Text(
|
||||
asset?.title??"",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
if (asset != null)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(999999999)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||
child: Text(
|
||||
asset?.title ?? "",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
if (subtitle.isNotEmpty)
|
||||
@@ -147,11 +149,15 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 4,
|
||||
children: [
|
||||
if(chainIconPath.isNotEmpty)
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width:16,height:16,colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),
|
||||
),
|
||||
if (chainIconPath.isNotEmpty)
|
||||
CakeImageWidget(
|
||||
imageUrl: chainIconPath,
|
||||
width: 16,
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
@@ -227,6 +233,18 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSwap && mode != AssetDetailsModalModes.ltcPrivate)
|
||||
AssetDetailsModalBottomButton(
|
||||
iconPath: "assets/new-ui/exchange.svg",
|
||||
title: S.of(context).swap,
|
||||
onPressed: () => openPage<NewSwapPage>(context, param2: asset),
|
||||
),
|
||||
if (showBridgeButton)
|
||||
AssetDetailsModalBottomButton(
|
||||
iconPath: "assets/new-ui/bridge.svg",
|
||||
title: "Bridge",
|
||||
onPressed: () => openPage<BridgeAmountPage>(context, param1: asset),
|
||||
),
|
||||
AssetDetailsModalBottomButton(
|
||||
iconPath: "assets/new-ui/receive.svg",
|
||||
title: S.of(context).receive,
|
||||
@@ -240,12 +258,6 @@ class AssetDetailsModal extends StatelessWidget {
|
||||
openPage<NewReceivePage>(context, param2: asset);
|
||||
},
|
||||
),
|
||||
if (showSwap && mode != AssetDetailsModalModes.ltcPrivate)
|
||||
AssetDetailsModalBottomButton(
|
||||
iconPath: "assets/new-ui/exchange.svg",
|
||||
title: S.of(context).swap,
|
||||
onPressed: () => openPage<NewSwapPage>(context, param2: asset),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox()
|
||||
|
||||
@@ -14,6 +14,7 @@ class AssetTile extends StatelessWidget {
|
||||
required this.balance,
|
||||
required this.chainIconPath,
|
||||
this.showSecondary = false,
|
||||
this.showBridgeButton = false,
|
||||
this.title,
|
||||
this.trailingText,
|
||||
this.modalMode = AssetDetailsModalModes.normal,
|
||||
@@ -22,6 +23,7 @@ class AssetTile extends StatelessWidget {
|
||||
final BalanceRecord balance;
|
||||
final bool showSecondary;
|
||||
final bool showSwap;
|
||||
final bool showBridgeButton;
|
||||
final String chainIconPath;
|
||||
final String? title;
|
||||
final String? trailingText;
|
||||
@@ -42,6 +44,7 @@ class AssetTile extends StatelessWidget {
|
||||
builder: (context) {
|
||||
return AssetDetailsModal(
|
||||
showSwap: showSwap,
|
||||
showBridgeButton: showBridgeButton,
|
||||
asset: balance.asset,
|
||||
title: title ?? balance.asset.fullName ?? balance.asset.name,
|
||||
chainTitle: "",
|
||||
|
||||
@@ -59,6 +59,7 @@ class AssetsSection extends StatelessWidget {
|
||||
final balance = dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
|
||||
return AssetTile(
|
||||
showSwap: dashboardViewModel.isEnabledSwapAction,
|
||||
showBridgeButton: dashboardViewModel.showBridge(balance.asset),
|
||||
balance: balance,
|
||||
wallet: dashboardViewModel.wallet,
|
||||
isFirst: index == 0,
|
||||
|
||||
@@ -23,6 +23,7 @@ class NewSendAddressInput extends StatefulWidget {
|
||||
this.validator,
|
||||
this.focusNode,
|
||||
this.displayName,
|
||||
this.hintText,
|
||||
});
|
||||
|
||||
final TextEditingController addressController;
|
||||
@@ -36,7 +37,8 @@ class NewSendAddressInput extends StatefulWidget {
|
||||
final bool bottomPadding;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final FocusNode? focusNode;
|
||||
|
||||
final String? hintText;
|
||||
|
||||
@override
|
||||
State<NewSendAddressInput> createState() => _NewSendAddressInputState();
|
||||
}
|
||||
@@ -96,7 +98,7 @@ class _NewSendAddressInputState extends State<NewSendAddressInput> {
|
||||
},
|
||||
controller: widget.addressController,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).search_or_enter,
|
||||
hintText: widget.hintText ?? S.of(context).search_or_enter,
|
||||
errorMaxLines: 3,
|
||||
),
|
||||
),
|
||||
|
||||
+21
-1
@@ -4,6 +4,10 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
|
||||
import 'package:cake_wallet/core/new_wallet_arguments.dart';
|
||||
import 'package:cake_wallet/new-ui/new_dashboard.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/about_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
|
||||
@@ -89,7 +93,6 @@ import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/pre_seed_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
|
||||
import 'package:cake_wallet/src/screens/seed/wallet_seed_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/send_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/send_template_page.dart';
|
||||
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
|
||||
import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
|
||||
@@ -136,6 +139,8 @@ import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
|
||||
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/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';
|
||||
@@ -1035,6 +1040,21 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
builder: (_) => getIt.get<DEuroSavingsPage>(),
|
||||
);
|
||||
|
||||
case Routes.bridgeHistoryPage:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => BridgeHistoryPage(settings.arguments as BridgeHistoryViewModel),
|
||||
);
|
||||
|
||||
case Routes.bridgeDestinationNetworkPage:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => BridgeNetworkPage(settings.arguments as BridgeViewModel),
|
||||
);
|
||||
|
||||
case Routes.bridgeReceivingWalletPage:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => BridgeReceivingWalletPage(settings.arguments as BridgeViewModel),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
|
||||
@@ -144,4 +144,8 @@ class Routes {
|
||||
|
||||
static const lightningUsernamePage = "/lightning_username_page";
|
||||
static const aboutPage = "/about_page";
|
||||
|
||||
static const bridgeHistoryPage = '/bridge_history_page';
|
||||
static const bridgeDestinationNetworkPage = '/bridge_destination_network_page';
|
||||
static const bridgeReceivingWalletPage = '/bridge_receiving_wallet_page';
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ class ListItemStyleWrapper extends StatelessWidget {
|
||||
this.backgroundColor,
|
||||
this.onTap,
|
||||
this.iconPath,
|
||||
this.contentPadding,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@@ -19,10 +20,10 @@ class ListItemStyleWrapper extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final Color? backgroundColor;
|
||||
final Widget Function(BuildContext context, TextStyle textStyle, TextStyle labelStyle) builder;
|
||||
final EdgeInsets? contentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final textStyle = TextStyle(
|
||||
@@ -70,15 +71,16 @@ class ListItemStyleWrapper extends StatelessWidget {
|
||||
child: Container(height: 1, color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
)
|
||||
else if(!isLastInSection) Container(
|
||||
else if (!isLastInSection)
|
||||
Container(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Container(height: 1, color: theme.colorScheme.outlineVariant),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:cake_wallet/entities/bridge_transfer.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'bridge_transfers_store.g.dart';
|
||||
|
||||
class BridgeTransfersStore = BridgeTransfersStoreBase with _$BridgeTransfersStore;
|
||||
|
||||
abstract class BridgeTransfersStoreBase with Store {
|
||||
BridgeTransfersStoreBase() : bridgeTransfers = [] {
|
||||
updateList();
|
||||
}
|
||||
|
||||
@observable
|
||||
List<BridgeTransfer> bridgeTransfers;
|
||||
|
||||
@action
|
||||
Future<void> updateList() async {
|
||||
bridgeTransfers = await BridgeTransfer.selectAll();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> addTransfer(BridgeTransfer transfer) async {
|
||||
try {
|
||||
await BridgeTransfer.insert(transfer);
|
||||
await updateList();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateTransfer(BridgeTransfer transfer) async {
|
||||
try {
|
||||
await BridgeTransfer.update(transfer);
|
||||
await updateList();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@computed
|
||||
List<BridgeTransfer> get activeTransfers =>
|
||||
bridgeTransfers.where((b) => b.isActive).toList(growable: false);
|
||||
|
||||
@computed
|
||||
List<BridgeTransfer> get pastTransfers =>
|
||||
bridgeTransfers.where((b) => !b.isActive).toList(growable: false);
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user