mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +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
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
Binary file not shown.
@@ -7,6 +7,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
|
||||
Database? db;
|
||||
|
||||
|
||||
|
||||
Future<void> _addColumnIfNotExists(
|
||||
Database db, {
|
||||
required String table,
|
||||
@@ -39,7 +41,7 @@ Future<void> initDb({String? pathOverride}) async {
|
||||
}
|
||||
}
|
||||
await db?.close();
|
||||
db = await openDatabase(dbFile.path, version: 4,
|
||||
db = await openDatabase(dbFile.path, version: 5,
|
||||
onUpgrade: (Database db, int oldVersion, int newVersion) async {
|
||||
printV("migrating: $oldVersion, $newVersion");
|
||||
if (oldVersion <= 1) {
|
||||
@@ -88,6 +90,9 @@ CREATE TABLE IF NOT EXISTS BalanceCardStyleSettings (
|
||||
// if address doesn't correspond to a valid token, fallback to primary token
|
||||
await _addColumnIfNotExists(db, table: "WalletInfo", column: "favoriteTokenAddress", definition: "TEXT DEFAULT NULL");
|
||||
}
|
||||
if (oldVersion <= 4) {
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
},
|
||||
onCreate: (Database db, int version) async {
|
||||
await db.execute(
|
||||
@@ -185,6 +190,7 @@ CREATE TABLE BalanceCardStyleSettings (
|
||||
FOREIGN KEY (walletInfoId) REFERENCES WalletInfo(walletInfoId)
|
||||
);
|
||||
''');
|
||||
await _createBridgeTransferTable(db);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -222,4 +228,31 @@ Future<Map<String, dynamic>> dumpCustomDb(String path) async {
|
||||
ret[tableName] = await db.query(tableName);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
Future<void> _createBridgeTransferTable(Database db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS BridgeTransfer (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
wallet_id TEXT NOT NULL,
|
||||
source_chain_id INTEGER NOT NULL,
|
||||
destination_chain_id INTEGER NOT NULL,
|
||||
token_symbol TEXT NOT NULL,
|
||||
token_contract TEXT NOT NULL,
|
||||
amount TEXT NOT NULL,
|
||||
recipient_address TEXT NOT NULL,
|
||||
source_tx_hash TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER,
|
||||
confirmed_at INTEGER,
|
||||
amount_raw TEXT,
|
||||
error_message TEXT,
|
||||
status_message TEXT
|
||||
);
|
||||
''');
|
||||
await db.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_bridgetransfer_wallet_id
|
||||
ON BridgeTransfer(wallet_id);
|
||||
''');
|
||||
}
|
||||
@@ -21,4 +21,4 @@ const HARDWARE_WALLET_TYPE_TYPE_ID = 19;
|
||||
const MWEB_UTXO_TYPE_ID = 20;
|
||||
const HAVEN_SEED_STORE_TYPE_ID = 21;
|
||||
const ZANO_ASSET_TYPE_ID = 22;
|
||||
const PAYJOIN_SESSION_TYPE_ID = 23;
|
||||
const PAYJOIN_SESSION_TYPE_ID = 23;
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:cw_evm/usdt0/usdt0_quote.dart';
|
||||
import 'package:hex/hex.dart' as hex;
|
||||
import 'package:web3dart/web3dart.dart' as web3;
|
||||
|
||||
/// Minimal OFT ABI for USDT0 (per docs.usdt0.to).
|
||||
/// quoteSend(SendParam, bool payInLzToken) returns MessagingFee.
|
||||
/// send(SendParam, MessagingFee, address refundAddress) payable.
|
||||
/// SendParam: dstEid, to (bytes32), amountLD (amount to send, token units),
|
||||
/// minAmountLD (min receive on destination), extraOptions, composeMsg, oftCmd.
|
||||
const String _oftAbiJson = '''
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{"internalType": "uint32", "name": "dstEid", "type": "uint32"},
|
||||
{"internalType": "bytes32", "name": "to", "type": "bytes32"},
|
||||
{"internalType": "uint256", "name": "amountLD", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "minAmountLD", "type": "uint256"},
|
||||
{"internalType": "bytes", "name": "extraOptions", "type": "bytes"},
|
||||
{"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
|
||||
{"internalType": "bytes", "name": "oftCmd", "type": "bytes"}
|
||||
],
|
||||
"internalType": "struct SendParam",
|
||||
"name": "sendParam",
|
||||
"type": "tuple"
|
||||
},
|
||||
{"internalType": "bool", "name": "payInLzToken", "type": "bool"}
|
||||
],
|
||||
"name": "quoteSend",
|
||||
"outputs": [
|
||||
{
|
||||
"components": [
|
||||
{"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"}
|
||||
],
|
||||
"internalType": "struct MessagingFee",
|
||||
"name": "",
|
||||
"type": "tuple"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{"internalType": "uint32", "name": "dstEid", "type": "uint32"},
|
||||
{"internalType": "bytes32", "name": "to", "type": "bytes32"},
|
||||
{"internalType": "uint256", "name": "amountLD", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "minAmountLD", "type": "uint256"},
|
||||
{"internalType": "bytes", "name": "extraOptions", "type": "bytes"},
|
||||
{"internalType": "bytes", "name": "composeMsg", "type": "bytes"},
|
||||
{"internalType": "bytes", "name": "oftCmd", "type": "bytes"}
|
||||
],
|
||||
"internalType": "struct SendParam",
|
||||
"name": "_sendParam",
|
||||
"type": "tuple"
|
||||
},
|
||||
{
|
||||
"components": [
|
||||
{"internalType": "uint256", "name": "nativeFee", "type": "uint256"},
|
||||
{"internalType": "uint256", "name": "lzTokenFee", "type": "uint256"}
|
||||
],
|
||||
"internalType": "struct MessagingFee",
|
||||
"name": "_fee",
|
||||
"type": "tuple"
|
||||
},
|
||||
{"internalType": "address", "name": "_refundAddress", "type": "address"}
|
||||
],
|
||||
"name": "send",
|
||||
"outputs": [],
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
''';
|
||||
|
||||
final web3.ContractAbi oftContractAbi = web3.ContractAbi.fromJson(_oftAbiJson, 'OFT');
|
||||
|
||||
/// OFT contract wrapper for quoteSend (view) and send (payable).
|
||||
class OFT {
|
||||
OFT({
|
||||
required web3.EthereumAddress address,
|
||||
required web3.Web3Client client,
|
||||
}) : _address = address,
|
||||
_client = client;
|
||||
|
||||
final web3.EthereumAddress _address;
|
||||
final web3.Web3Client _client;
|
||||
|
||||
/// Calls quoteSend and returns MessagingFee (nativeFee, lzTokenFee).
|
||||
Future<USDT0Quote> quoteSend({
|
||||
required int dstEid,
|
||||
required List<int> toBytes32,
|
||||
required BigInt amountLD,
|
||||
required BigInt minAmountLD,
|
||||
Uint8List? extraOptions,
|
||||
Uint8List? composeMsg,
|
||||
Uint8List? oftCmd,
|
||||
bool payInLzToken = false,
|
||||
}) async {
|
||||
final contract = web3.DeployedContract(
|
||||
oftContractAbi,
|
||||
_address,
|
||||
);
|
||||
final fn = contract.function('quoteSend');
|
||||
final sendParam = [
|
||||
BigInt.from(dstEid),
|
||||
Uint8List.fromList(toBytes32),
|
||||
amountLD,
|
||||
minAmountLD,
|
||||
extraOptions ?? Uint8List(0),
|
||||
composeMsg ?? Uint8List(0),
|
||||
oftCmd ?? Uint8List(0),
|
||||
];
|
||||
final params = [sendParam, payInLzToken];
|
||||
|
||||
final result = await _client.call(
|
||||
contract: contract,
|
||||
function: fn,
|
||||
params: params,
|
||||
);
|
||||
|
||||
final msgFee = result.first as List<dynamic>;
|
||||
final nativeFee = msgFee[0] as BigInt;
|
||||
final lzTokenFee = msgFee[1] as BigInt;
|
||||
return USDT0Quote(nativeFee: nativeFee, lzTokenFee: lzTokenFee);
|
||||
}
|
||||
|
||||
/// Encodes send(SendParam, MessagingFee, refundAddress) for transaction.
|
||||
/// Returns (dataHex, valueWei) where valueWei is the native fee to send.
|
||||
({String dataHex, BigInt valueWei}) encodeSend({
|
||||
required int dstEid,
|
||||
required List<int> toBytes32,
|
||||
required BigInt amountLD,
|
||||
required BigInt minAmountLD,
|
||||
required BigInt nativeFee,
|
||||
required BigInt lzTokenFee,
|
||||
required String refundAddress,
|
||||
Uint8List? extraOptions,
|
||||
Uint8List? composeMsg,
|
||||
Uint8List? oftCmd,
|
||||
}) {
|
||||
final contract = web3.DeployedContract(oftContractAbi, _address);
|
||||
final fn = contract.function('send');
|
||||
final sendParam = [
|
||||
BigInt.from(dstEid),
|
||||
Uint8List.fromList(toBytes32),
|
||||
amountLD,
|
||||
minAmountLD,
|
||||
extraOptions ?? Uint8List(0),
|
||||
composeMsg ?? Uint8List(0),
|
||||
oftCmd ?? Uint8List(0),
|
||||
];
|
||||
final fee = [nativeFee, lzTokenFee];
|
||||
final refund = web3.EthereumAddress.fromHex(refundAddress);
|
||||
final encoded = fn.encodeCall([sendParam, fee, refund]);
|
||||
final dataHex = '0x${hex.HEX.encode(encoded)}';
|
||||
return (dataHex: dataHex, valueWei: nativeFee);
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts EVM address (0x + 40 hex) to bytes32 (left-padded).
|
||||
List<int> addressToBytes32(String address) {
|
||||
final clean = address.startsWith('0x') ? address.substring(2) : address;
|
||||
|
||||
if (clean.length != 40) throw ArgumentError('Invalid address length');
|
||||
|
||||
return hex.HEX.decode('000000000000000000000000$clean');
|
||||
}
|
||||
@@ -1281,12 +1281,25 @@ abstract class EVMChainWalletBase
|
||||
} else if (newTxInfo.direction == TransactionDirection.incoming &&
|
||||
existingTxInfo.direction == TransactionDirection.outgoing) {
|
||||
result[transactionModel.hash] = newTxInfo;
|
||||
}
|
||||
|
||||
else if (newTxInfo.direction == TransactionDirection.outgoing &&
|
||||
existingTxInfo.direction == TransactionDirection.outgoing &&
|
||||
_hasEvmTokenContractAddress(newTxInfo) &&
|
||||
!_hasEvmTokenContractAddress(existingTxInfo)) {
|
||||
result[transactionModel.hash] = newTxInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool _hasEvmTokenContractAddress(EVMChainTransactionInfo info) {
|
||||
final c = info.contractAddress;
|
||||
return c != null && c.isNotEmpty;
|
||||
}
|
||||
|
||||
String? analyzeTransaction(String? transactionInput) {
|
||||
if (transactionInput == '0x' || transactionInput == null || transactionInput.isEmpty) {
|
||||
return '';
|
||||
|
||||
@@ -12,6 +12,13 @@ class ArbitrumTokens {
|
||||
decimal: 18,
|
||||
enabled: true,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "Tether USD (Omnichain)",
|
||||
symbol: "USDT",
|
||||
contractAddress: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
|
||||
decimal: 6,
|
||||
enabled: true,
|
||||
),
|
||||
Erc20Token(
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
@@ -68,8 +75,7 @@ class ArbitrumTokens {
|
||||
if (token.iconPath?.isEmpty ?? true) {
|
||||
try {
|
||||
iconPath = CryptoCurrency.all
|
||||
.firstWhere((element) =>
|
||||
element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
|
||||
.iconPath;
|
||||
} catch (_) {}
|
||||
} else {
|
||||
@@ -80,4 +86,3 @@ class ArbitrumTokens {
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
|
||||
/// USDT0 (Omnichain USDT) config.
|
||||
/// Addresses and EIDs from https://docs.usdt0.to/technical-documentation/developer/usdt0-deployments
|
||||
class USDT0Config {
|
||||
USDT0Config._();
|
||||
|
||||
static const String ethereumOftAdapter =
|
||||
'0x6C96dE32CEa08842dcc4058c14d3aaAD7Fa41dee';
|
||||
|
||||
static const String ethereumNativeUsdt =
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7';
|
||||
|
||||
static const Map<int, String> usdt0TokenAddressByChainId = {
|
||||
1: ethereumNativeUsdt,
|
||||
137: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
||||
42161: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9',
|
||||
};
|
||||
|
||||
static const Map<int, String> oftContractAddressByChainId = {
|
||||
1: ethereumOftAdapter,
|
||||
137: '0x6BA10300f0DC58B7a1e4c0e41f5daBb7D7829e13',
|
||||
42161: '0x14E4A1B13bf7F943c8ff7C51fb60FA964A298D92',
|
||||
};
|
||||
|
||||
static const Map<int, int> endpointIdByChainId = {
|
||||
1: 30101,
|
||||
137: 30109,
|
||||
42161: 30110,
|
||||
};
|
||||
|
||||
static const int ethereumChainId = 1;
|
||||
|
||||
static List<int> get supportedChainIds =>
|
||||
usdt0TokenAddressByChainId.keys.toList(growable: false);
|
||||
|
||||
static String? getOftAdapterAddress(int chainId) {
|
||||
if (chainId == ethereumChainId) return ethereumOftAdapter;
|
||||
return null;
|
||||
}
|
||||
|
||||
static String? getUsdt0TokenAddress(int chainId) =>
|
||||
usdt0TokenAddressByChainId[chainId];
|
||||
|
||||
static String? getOftContractAddress(int chainId) =>
|
||||
oftContractAddressByChainId[chainId];
|
||||
|
||||
static int? getEndpointId(int chainId) => endpointIdByChainId[chainId];
|
||||
|
||||
static bool isChainSupported(int chainId) =>
|
||||
usdt0TokenAddressByChainId.containsKey(chainId);
|
||||
|
||||
static bool isUSDT0Token(Erc20Token token, int chainId) {
|
||||
final address = getUsdt0TokenAddress(chainId);
|
||||
if (address == null) return false;
|
||||
return token.contractAddress.toLowerCase() == address.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
class USDT0Quote {
|
||||
const USDT0Quote({
|
||||
required this.nativeFee,
|
||||
required this.lzTokenFee,
|
||||
});
|
||||
|
||||
/// Fee in native token (e.g. ETH, MATIC) in wei.
|
||||
final BigInt nativeFee;
|
||||
|
||||
/// Fee in LZ token (if paying with LZ token).
|
||||
final BigInt lzTokenFee;
|
||||
|
||||
@override
|
||||
String toString() => 'USDT0Quote(nativeFee: $nativeFee, lzTokenFee: $lzTokenFee)';
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_evm/contract/erc20.dart';
|
||||
import 'package:cw_evm/contract/oft.dart';
|
||||
import 'package:cw_evm/evm_chain_transaction_priority.dart';
|
||||
import 'package:cw_evm/utils/evm_chain_utils.dart';
|
||||
import 'package:cw_evm/evm_chain_wallet.dart';
|
||||
import 'package:cw_evm/usdt0/usdt0_config.dart';
|
||||
import 'package:cw_evm/usdt0/usdt0_quote.dart';
|
||||
import 'package:web3dart/web3dart.dart' as web3;
|
||||
|
||||
class USDT0Service {
|
||||
static Future<USDT0Quote> quoteCrossChainTransfer({
|
||||
required web3.Web3Client client,
|
||||
required int sourceChainId,
|
||||
required int destinationChainId,
|
||||
required BigInt amount,
|
||||
required String recipientAddress,
|
||||
BigInt? minAmount,
|
||||
}) async {
|
||||
final min = minAmount ?? BigInt.zero;
|
||||
final oftAddress = USDT0Config.getOftContractAddress(sourceChainId);
|
||||
final dstEid = USDT0Config.getEndpointId(destinationChainId);
|
||||
|
||||
if (oftAddress == null || dstEid == null) {
|
||||
throw Exception(
|
||||
'USDT0 not supported for chain $sourceChainId -> $destinationChainId',
|
||||
);
|
||||
}
|
||||
|
||||
final toBytes32 = addressToBytes32(recipientAddress);
|
||||
final oft = OFT(
|
||||
address: web3.EthereumAddress.fromHex(oftAddress),
|
||||
client: client,
|
||||
);
|
||||
|
||||
return oft.quoteSend(
|
||||
dstEid: dstEid,
|
||||
toBytes32: toBytes32,
|
||||
amountLD: amount,
|
||||
minAmountLD: min,
|
||||
payInLzToken: false,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<PendingTransaction> executeCrossChainTransfer({
|
||||
required EVMChainWallet wallet,
|
||||
required int sourceChainId,
|
||||
required int destinationChainId,
|
||||
required BigInt amount,
|
||||
required String recipientAddress,
|
||||
required USDT0Quote quote,
|
||||
required Erc20Token token,
|
||||
required EVMChainTransactionPriority priority,
|
||||
bool useBlinkProtection = true,
|
||||
}) async {
|
||||
final oftAddress = USDT0Config.getOftContractAddress(sourceChainId);
|
||||
final dstEid = USDT0Config.getEndpointId(destinationChainId);
|
||||
|
||||
if (oftAddress == null || dstEid == null) {
|
||||
throw Exception(
|
||||
'USDT0 not supported for chain $sourceChainId -> $destinationChainId',
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceChainId == USDT0Config.ethereumChainId) {
|
||||
final adapter = USDT0Config.getOftAdapterAddress(sourceChainId);
|
||||
if (adapter != null) {
|
||||
final needsApproval = await _isApprovalRequired(
|
||||
wallet: wallet,
|
||||
token: token,
|
||||
spender: adapter,
|
||||
amount: amount,
|
||||
);
|
||||
|
||||
if (needsApproval) {
|
||||
final maxUint = BigInt.parse(
|
||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
radix: 16,
|
||||
);
|
||||
|
||||
final pendingApproval = await wallet.createApprovalTransaction(
|
||||
maxUint,
|
||||
adapter,
|
||||
token,
|
||||
priority,
|
||||
EVMChainUtils.getFeeCurrency(wallet.selectedChainId),
|
||||
useBlinkProtection: useBlinkProtection,
|
||||
);
|
||||
await pendingApproval.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final client = wallet.getWeb3Client();
|
||||
if (client == null) {
|
||||
throw StateError('Wallet not connected to node');
|
||||
}
|
||||
|
||||
final toBytes32 = addressToBytes32(recipientAddress);
|
||||
final oft = OFT(
|
||||
address: web3.EthereumAddress.fromHex(oftAddress),
|
||||
client: client,
|
||||
);
|
||||
|
||||
final encoded = oft.encodeSend(
|
||||
dstEid: dstEid,
|
||||
toBytes32: toBytes32,
|
||||
amountLD: amount,
|
||||
minAmountLD: BigInt.zero,
|
||||
nativeFee: quote.nativeFee,
|
||||
lzTokenFee: quote.lzTokenFee,
|
||||
refundAddress: wallet.walletAddresses.primaryAddress,
|
||||
);
|
||||
|
||||
return wallet.createCallDataTransaction(
|
||||
oftAddress,
|
||||
encoded.dataHex,
|
||||
encoded.valueWei,
|
||||
priority,
|
||||
token.contractAddress,
|
||||
amount,
|
||||
useBlinkProtection: useBlinkProtection,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> _isApprovalRequired({
|
||||
required EVMChainWallet wallet,
|
||||
required Erc20Token token,
|
||||
required String spender,
|
||||
required BigInt amount,
|
||||
}) async {
|
||||
final client = wallet.getWeb3Client();
|
||||
if (client == null) return true;
|
||||
|
||||
final erc20 = ERC20(
|
||||
address: web3.EthereumAddress.fromHex(token.contractAddress),
|
||||
client: client,
|
||||
);
|
||||
final current = await erc20.allowance(
|
||||
web3.EthereumAddress.fromHex(wallet.walletAddresses.primaryAddress),
|
||||
web3.EthereumAddress.fromHex(spender),
|
||||
);
|
||||
return current < amount;
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M30.8571 20.5713H26.2857V12.1913C27.3844 13.5466 28.8102 14.5997 30.4286 15.2513C30.7083 15.3591 31.0192 15.3526 31.2943 15.2333C31.5693 15.114 31.7865 14.8914 31.8989 14.6135C32.0114 14.3356 32.0101 14.0246 31.8954 13.7476C31.7807 13.4706 31.5617 13.2498 31.2857 13.1327C29.8056 12.5386 28.5378 11.5139 27.6464 10.1913C26.755 8.86873 26.281 7.30908 26.2857 5.71415C26.2857 5.41104 26.1653 5.12035 25.951 4.90602C25.7367 4.6917 25.446 4.57129 25.1429 4.57129C24.8398 4.57129 24.5491 4.6917 24.3347 4.90602C24.1204 5.12035 24 5.41104 24 5.71415C24 7.83588 23.1571 9.87071 21.6569 11.371C20.1566 12.8713 18.1217 13.7141 16 13.7141C13.8783 13.7141 11.8434 12.8713 10.3431 11.371C8.84286 9.87071 8 7.83588 8 5.71415C8 5.41104 7.87959 5.12035 7.66527 4.90602C7.45094 4.6917 7.16025 4.57129 6.85714 4.57129C6.55404 4.57129 6.26335 4.6917 6.04902 4.90602C5.83469 5.12035 5.71429 5.41104 5.71429 5.71415C5.71901 7.30908 5.245 8.86873 4.3536 10.1913C3.4622 11.5139 2.19442 12.5386 0.714286 13.1327C0.438277 13.2498 0.219319 13.4706 0.104613 13.7476C-0.010094 14.0246 -0.0113626 14.3356 0.10108 14.6135C0.213523 14.8914 0.430672 15.114 0.705716 15.2333C0.98076 15.3526 1.29167 15.3591 1.57143 15.2513C3.18985 14.5997 4.61563 13.5466 5.71429 12.1913V20.5713H1.14286C0.839753 20.5713 0.549062 20.6917 0.334735 20.906C0.120408 21.1204 0 21.411 0 21.7141C0 22.0173 0.120408 22.3079 0.334735 22.5223C0.549062 22.7366 0.839753 22.857 1.14286 22.857H5.71429V26.2856C5.71429 26.5887 5.83469 26.8794 6.04902 27.0937C6.26335 27.308 6.55404 27.4284 6.85714 27.4284C7.16025 27.4284 7.45094 27.308 7.66527 27.0937C7.87959 26.8794 8 26.5887 8 26.2856V22.857H24V26.2856C24 26.5887 24.1204 26.8794 24.3347 27.0937C24.5491 27.308 24.8398 27.4284 25.1429 27.4284C25.446 27.4284 25.7367 27.308 25.951 27.0937C26.1653 26.8794 26.2857 26.5887 26.2857 26.2856V22.857H30.8571C31.1602 22.857 31.4509 22.7366 31.6653 22.5223C31.8796 22.3079 32 22.0173 32 21.7141C32 21.411 31.8796 21.1204 31.6653 20.906C31.4509 20.6917 31.1602 20.5713 30.8571 20.5713ZM18.2857 15.7427V20.5713H13.7143V15.7427C15.2188 16.0856 16.7812 16.0856 18.2857 15.7427ZM8 12.1713C8.93266 13.3237 10.1019 14.2625 11.4286 14.9241V20.5713H8V12.1713ZM20.5714 20.5713V14.9256C21.8981 14.2639 23.0673 13.3251 24 12.1727V20.5713H20.5714Z" fill="#91B0FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
+34
-1
@@ -1392,7 +1392,13 @@ import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
|
||||
import 'package:cw_evm/utils/evm_chain_utils.dart';
|
||||
import 'package:cw_evm/evm_chain_default_tokens.dart';
|
||||
import 'package:cw_evm/deuro/deuro_savings.dart';
|
||||
import 'package:cw_evm/usdt0/usdt0_config.dart';
|
||||
import 'package:cw_evm/usdt0/usdt0_quote.dart';
|
||||
import 'package:cw_evm/usdt0/usdt0_service.dart';
|
||||
import 'package:eth_sig_util/util/utils.dart';
|
||||
export 'package:cw_evm/evm_chain_transaction_priority.dart';
|
||||
export 'package:cw_evm/evm_erc20_balance.dart';
|
||||
export 'package:cw_evm/usdt0/usdt0_quote.dart';
|
||||
|
||||
""";
|
||||
const evmCwPart = "part 'cw_evm.dart';";
|
||||
@@ -1546,19 +1552,44 @@ abstract class EVM {
|
||||
WalletType? getWalletTypeByChainId(int chainId);
|
||||
String getChainNameByChainId(int chainId);
|
||||
String getTokenNameByChainId(int chainId);
|
||||
|
||||
// Chain selection methods
|
||||
List<ChainInfo> getAllChains();
|
||||
ChainInfo? getCurrentChain(WalletBase wallet);
|
||||
ChainInfo? getChainInfoByChainId(int chainId);
|
||||
|
||||
|
||||
int? getSelectedChainId(WalletBase wallet);
|
||||
Future<void> selectChain(WalletBase wallet, int chainId, {required Node node});
|
||||
|
||||
String? getExplorerUrlForChainId(int chainId, {bool showProtocol = true});
|
||||
|
||||
Future<bool?> getTransactionReceipt(WalletBase wallet, String txHash);
|
||||
|
||||
bool hasPriorityFee(int chainId);
|
||||
|
||||
bool isUSDT0Token(WalletBase wallet, CryptoCurrency token);
|
||||
List<ChainInfo> getUSDT0DestinationChains(WalletBase wallet);
|
||||
|
||||
Future<USDT0Quote> quoteUSDT0Transfer({
|
||||
required WalletBase wallet,
|
||||
required int sourceChainId,
|
||||
required int destinationChainId,
|
||||
required BigInt amount,
|
||||
required String recipientAddress,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
Future<EvmWalletConnectFeeQuote?> getWCBufferedFeeQuote(
|
||||
WalletBase wallet,
|
||||
TransactionPriority priority,
|
||||
@@ -1572,11 +1603,13 @@ class ChainInfo {
|
||||
required this.chainId,
|
||||
required this.name,
|
||||
required this.shortCode,
|
||||
required this.currency,
|
||||
});
|
||||
|
||||
final int chainId;
|
||||
final String name;
|
||||
final String shortCode;
|
||||
final CryptoCurrency currency;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
|
||||
Reference in New Issue
Block a user