diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index 99a489bb1..b9b021896 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -116,10 +116,18 @@ jobs: ./scripts/prepare_moneroc.sh mkdir -p scripts/monero_c/release pushd scripts/monero_c/release - mkdir -p $(git describe --tags) - pushd $(git describe --tags) + TAG=$(git describe --tags) + mkdir -p "$TAG" + pushd "$TAG" wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip unzip release-bundle.zip + # RC2's bundle puts everything under an inner v0.18.4.6-*/ dir, strip it + shopt -s nullglob + for inner in v0.18.4.6-*/; do + mv "$inner"* . + rmdir "$inner" + done + shopt -u nullglob rm release-bundle.zip popd popd diff --git a/cw_core/lib/payment_uris.dart b/cw_core/lib/payment_uris.dart index 6ad43a875..daf56af2f 100644 --- a/cw_core/lib/payment_uris.dart +++ b/cw_core/lib/payment_uris.dart @@ -176,14 +176,23 @@ class PolygonURI extends PaymentURI { } class SolanaURI extends PaymentURI { - SolanaURI({required super.amount, required super.address}); + SolanaURI({required super.amount, required super.address, this.contractAddress}); + + final String? contractAddress; @override String toString() { var base = 'solana:$address'; + final params = []; if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; + params.add('amount=${amount.replaceAll(',', '.')}'); + } + if (contractAddress != null && contractAddress!.isNotEmpty) { + params.add('spl-token=$contractAddress'); + } + if (params.isNotEmpty) { + base += '?${params.join('&')}'; } return base; @@ -191,14 +200,23 @@ class SolanaURI extends PaymentURI { } class TronURI extends PaymentURI { - TronURI({required super.amount, required super.address}); + TronURI({required super.amount, required super.address, this.contractAddress}); + + final String? contractAddress; @override String toString() { var base = 'tron:$address'; + final params = []; if (amount.isNotEmpty) { - base += '?amount=${amount.replaceAll(',', '.')}'; + params.add('amount=${amount.replaceAll(',', '.')}'); + } + if (contractAddress != null && contractAddress!.isNotEmpty) { + params.add('token=$contractAddress'); + } + if (params.isNotEmpty) { + base += '?${params.join('&')}'; } return base; diff --git a/cw_solana/lib/solana_transaction_info.dart b/cw_solana/lib/solana_transaction_info.dart index f51c55dad..85c28ac07 100644 --- a/cw_solana/lib/solana_transaction_info.dart +++ b/cw_solana/lib/solana_transaction_info.dart @@ -28,6 +28,9 @@ class SolanaTransactionInfo extends TransactionInfo { String? _fiatAmount; + @override + String get txHash => id.replaceFirst(RegExp(r'_(outgoing|incoming)$'), ''); + @override DateTime get date => blockTime; diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart index af09b68ab..fec5e06f5 100644 --- a/lib/new-ui/pages/send_page.dart +++ b/lib/new-ui/pages/send_page.dart @@ -228,6 +228,14 @@ class _NewSendPageState extends State { _addressControllers[0].text = widget.initialPaymentRequest!.address; _amountControllers[0].text = widget.initialPaymentRequest!.amount; _memoControllers[0].text = widget.initialPaymentRequest!.note; + final contractAddress = widget.initialPaymentRequest!.contractAddress; + if (contractAddress != null && contractAddress.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + widget.sendViewModel.fetchTokenForContractAddress(contractAddress); + } + }); + } } /// if the current wallet doesn't match the one in the qr code diff --git a/lib/router.dart b/lib/router.dart index 89365171a..a08f78e4d 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -363,7 +363,7 @@ Route createRoute(RouteSettings settings) { arguments: [availableWalletTypes.first, hardwareWalletType]), isReconnect: false, ), - getIt.get(), + getIt.get(param1: hardwareWalletType), ), ); } diff --git a/lib/src/screens/buy/buy_sell_page.dart b/lib/src/screens/buy/buy_sell_page.dart index 1a3c181cf..73c238b57 100644 --- a/lib/src/screens/buy/buy_sell_page.dart +++ b/lib/src/screens/buy/buy_sell_page.dart @@ -264,6 +264,10 @@ class BuySellPage extends BasePage { final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController; final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController; + cryptoAddressController.addListener(() { + buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text); + }); + _onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey); _onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey); @@ -313,10 +317,6 @@ class BuySellPage extends BasePage { } }); - cryptoAddressController.addListener(() { - buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text); - }); - _cryptoAddressFocus.addListener(() async { if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) { final domain = cryptoAddressController.text; @@ -325,7 +325,7 @@ class BuySellPage extends BasePage { } }); - reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) { + reaction((_) => buySellViewModel.wallet.walletAddresses.addressForBuy, (String address) { if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) { cryptoCurrencyKey.currentState!.changeAddress(address: address); } @@ -359,7 +359,7 @@ class BuySellPage extends BasePage { key.currentState!.changeAddress( address: - isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : ''); + isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForBuy : ''); key.currentState!.changeAmount(amount: ''); } @@ -371,9 +371,9 @@ class BuySellPage extends BasePage { if (isCurrentTypeWallet) { key.currentState!.changeWalletName(buySellViewModel.wallet.name); key.currentState!.addressController.text = - buySellViewModel.wallet.walletAddresses.addressForExchange; + buySellViewModel.wallet.walletAddresses.addressForBuy; } else if (key.currentState!.addressController.text == - buySellViewModel.wallet.walletAddresses.addressForExchange) { + buySellViewModel.wallet.walletAddresses.addressForBuy) { key.currentState!.changeWalletName(''); key.currentState!.addressController.text = ''; } @@ -429,7 +429,7 @@ class BuySellPage extends BasePage { initialCurrency: buySellViewModel.cryptoCurrency, initialWalletName: '', initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency - ? buySellViewModel.wallet.walletAddresses.addressForExchange + ? buySellViewModel.wallet.walletAddresses.addressForBuy : buySellViewModel.cryptoCurrencyAddress, initialIsAmountEditable: true, isAmountEstimated: true, diff --git a/lib/utils/payment_request.dart b/lib/utils/payment_request.dart index c0d747080..fdc48b1eb 100644 --- a/lib/utils/payment_request.dart +++ b/lib/utils/payment_request.dart @@ -60,6 +60,16 @@ class PaymentRequest { address = paymentUri.address; amount = paymentUri.amount; contractAddress = paymentUri.contractAddress; + } else if (scheme == "tron") { + final token = uri.queryParameters['token']; + if (token != null && token.isNotEmpty) { + contractAddress = token; + } + } else if (scheme == "solana") { + final splToken = uri.queryParameters['spl-token']; + if (splToken != null && splToken.isNotEmpty) { + contractAddress = splToken; + } } } diff --git a/lib/utils/token_utilities.dart b/lib/utils/token_utilities.dart index 8e70e3e28..34dc393a1 100644 --- a/lib/utils/token_utilities.dart +++ b/lib/utils/token_utilities.dart @@ -144,7 +144,7 @@ class TokenUtilities { return result; } - /// Finds a token by address across wallets depending on [walletType] + /// Finds a token by address for the given [walletType]. /// - EVM chains: match by contractAddress /// - Solana: match by mintAddress /// - Tron: match by contractAddress @@ -152,34 +152,15 @@ class TokenUtilities { required WalletType walletType, required String address, }) async { - if(address.isEmpty) return null; + if (address.isEmpty) return null; final lower = address.toLowerCase(); - switch (walletType) { - case WalletType.ethereum: - case WalletType.polygon: - case WalletType.base: - case WalletType.arbitrum: - case WalletType.bsc: - final tokens = await loadAllUniqueEvmTokens(); - for (final t in tokens) { - if (t.contractAddress.toLowerCase() == lower) return t; - } - return null; - case WalletType.solana: - final solTokens = await loadAllUniqueSolTokens(); - for (final t in solTokens) { - if (t.mintAddress.toLowerCase() == lower) return t; - } - return null; - case WalletType.tron: - final tronTokens = await loadAllUniqueTronTokens(); - for (final t in tronTokens) { - if (t.contractAddress.toLowerCase() == lower) return t; - } - return null; - default: - return null; + final tokens = await getAvailableTokensForNetwork(walletType); + for (final t in tokens) { + if (t is Erc20Token && t.contractAddress.toLowerCase() == lower) return t; + if (t is SPLToken && t.mintAddress.toLowerCase() == lower) return t; + if (t is TronToken && t.contractAddress.toLowerCase() == lower) return t; } + return null; } static Future> _openEvmTokensBoxFor(WalletInfo walletInfo) async { diff --git a/lib/view_model/dashboard/nft_view_model.dart b/lib/view_model/dashboard/nft_view_model.dart index 5b7a2fdcc..dda460b90 100644 --- a/lib/view_model/dashboard/nft_view_model.dart +++ b/lib/view_model/dashboard/nft_view_model.dart @@ -7,6 +7,7 @@ import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart'; import 'package:cake_wallet/evm/evm.dart'; +import 'package:cake_wallet/utils/debounce.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:mobx/mobx.dart'; @@ -31,12 +32,14 @@ abstract class NFTViewModelBase with Store { if (wallet != null) return wallet.chainId; return null; - }, (_) => getNFTAssetByWallet()); + }, (_) => _fetchDebounce.run(() => getNFTAssetByWallet())); } } final AppStore appStore; + bool _nftErrorPopupShown = false; final BottomSheetService bottomSheetService; + final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500)); @observable bool isLoading; @@ -100,6 +103,8 @@ abstract class NFTViewModelBase with Store { final decodedResponse = jsonDecode(response.body); + _nftErrorPopupShown = false; + if (wallet.type == WalletType.solana) { final results = await Future.wait( (decodedResponse as List).map( @@ -115,7 +120,8 @@ abstract class NFTViewModelBase with Store { solanaNftAssetModels.addAll(results); } else { - final result = WalletNFTsResponseModel.fromJson(decodedResponse as Map).result ?? []; + final result = + WalletNFTsResponseModel.fromJson(decodedResponse as Map).result ?? []; nftAssetByWalletModels.clear(); @@ -123,12 +129,15 @@ abstract class NFTViewModelBase with Store { } } catch (e) { log(e.toString()); - bottomSheetService.queueBottomSheet( - isModalDismissible: true, - widget: BottomSheetMessageDisplayWidget( - message: S.current.moralis_nft_error, - ), - ); + if (!_nftErrorPopupShown) { + _nftErrorPopupShown = true; + bottomSheetService.queueBottomSheet( + isModalDismissible: true, + widget: BottomSheetMessageDisplayWidget( + message: S.current.moralis_nft_error, + ), + ); + } } finally { isLoading = false; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 03d0ef4db..5fb896f53 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -1071,6 +1071,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } if (depositCurrency == CryptoCurrency.btcln && + wallet.type == WalletType.bitcoin && depositAddress == wallet.walletAddresses.addressForExchange) { final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); if (invoice != null) { @@ -1079,6 +1080,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with } if (receiveCurrency == CryptoCurrency.btcln && + wallet.type == WalletType.bitcoin && receiveAddress == wallet.walletAddresses.addressForExchange) { final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero); if (invoice != null) { diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 89c5374f7..f097e5d12 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -34,6 +34,8 @@ import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/payment_uris.dart'; +import 'package:cw_core/spl_token.dart'; +import 'package:cw_core/tron_token.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -202,6 +204,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo amount: _amount, contractAddress: (tokenCurrency as Erc20Token).contractAddress); } + if (tokenCurrency is TronToken && wallet.type == WalletType.tron) { + return TronURI( + amount: _amount, + address: wallet.walletAddresses.address, + contractAddress: (tokenCurrency as TronToken).contractAddress, + ); + } + if (tokenCurrency is SPLToken && wallet.type == WalletType.solana) { + return SolanaURI( + amount: _amount, + address: wallet.walletAddresses.address, + contractAddress: (tokenCurrency as SPLToken).mintAddress, + ); + } if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!; return wallet.walletAddresses.getPaymentUri(_amount); }