diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 03c44755c..b4ddcfefe 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -151,6 +151,7 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const quantexExchangeMarkup = '${{ secrets.QUANTEX_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart echo "const nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/lib/.secrets.g.dart echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart diff --git a/assets/images/quantex.png b/assets/images/quantex.png new file mode 100644 index 000000000..cfa32b382 Binary files /dev/null and b/assets/images/quantex.png differ diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index d5297ebe1..faad67777 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1 +1 @@ -Generic bug fixes and enhancements \ No newline at end of file +Bug fixes and generic enhancements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index f9b05cea2..faad67777 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,3 +1 @@ -Hardware wallets support for Bitcoin, Ethereum and Polygon -Security enhancements Bug fixes and generic enhancements \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0cc57e075..c4ee98c37 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -307,7 +307,7 @@ SPEC CHECKSUMS: Toast: ec33c32b8688982cecc6348adeae667c1b9938da uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 4d9691035..c28de5b72 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -22,10 +22,11 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable< ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); static const exolix = ExchangeProviderDescription(title: 'Exolix', raw: 6, image: 'assets/images/exolix.png'); - static const thorChain = - ExchangeProviderDescription(title: 'ThorChain' , raw: 8, image: 'assets/images/thorchain.png'); - static const all = ExchangeProviderDescription(title: 'All trades', raw: 7, image: ''); + static const thorChain = + ExchangeProviderDescription(title: 'ThorChain', raw: 8, image: 'assets/images/thorchain.png'); + static const quantex = + ExchangeProviderDescription(title: 'Quantex', raw: 9, image: 'assets/images/quantex.png'); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -43,10 +44,12 @@ class ExchangeProviderDescription extends EnumerableItem<int> with Serializable< return trocador; case 6: return exolix; - case 8: - return thorChain; case 7: return all; + case 8: + return thorChain; + case 9: + return quantex; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); } diff --git a/lib/exchange/provider/quantex_exchange_provider.dart b/lib/exchange/provider/quantex_exchange_provider.dart new file mode 100644 index 000000000..9ab7fbb55 --- /dev/null +++ b/lib/exchange/provider/quantex_exchange_provider.dart @@ -0,0 +1,252 @@ +import 'dart:convert'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trade_not_created_exception.dart'; +import 'package:cake_wallet/exchange/trade_not_found_exception.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:http/http.dart'; + +class QuantexExchangeProvider extends ExchangeProvider { + QuantexExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + + static final List<CryptoCurrency> _notSupported = [ + ...(CryptoCurrency.all + .where((element) => ![ + CryptoCurrency.btc, + CryptoCurrency.sol, + CryptoCurrency.eth, + CryptoCurrency.ltc, + CryptoCurrency.ada, + CryptoCurrency.bch, + CryptoCurrency.usdt, + CryptoCurrency.bnb, + CryptoCurrency.xmr, + ].contains(element)) + .toList()) + ]; + + static final markup = secrets.quantexExchangeMarkup; + + static const apiAuthority = 'api.myquantex.com'; + static const getRate = '/api/swap/get-rate'; + static const getCoins = '/api/swap/get-coins'; + static const createOrder = '/api/swap/create-order'; + + @override + String get title => 'Quantex'; + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => false; + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.quantex; + + @override + Future<bool> checkIsAvailable() async => true; + + @override + Future<Limits> fetchLimits({ + required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode, + }) async { + try { + final uri = Uri.https(apiAuthority, getCoins); + final response = await get(uri); + + final responseJSON = json.decode(response.body) as Map<String, dynamic>; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final coinsInfo = responseJSON['data'] as List<dynamic>; + + for (var coin in coinsInfo) { + if (coin['id'].toString().toUpperCase() == _normalizeCurrency(from)) { + return Limits( + min: double.parse(coin['min'].toString()), + max: double.parse(coin['max'].toString()), + ); + } + } + + // coin not found: + return Limits(min: 0, max: 0); + } catch (e) { + print(e.toString()); + return Limits(min: 0, max: 0); + } + } + + @override + Future<double> fetchRate({ + required CryptoCurrency from, + required CryptoCurrency to, + required double amount, + required bool isFixedRateMode, + required bool isReceiveAmount, + }) async { + try { + if (amount == 0) return 0.0; + + final headers = <String, String>{}; + final params = <String, dynamic>{}; + final body = <String, String>{ + 'coin_send': _normalizeCurrency(from), + 'coin_receive': _normalizeCurrency(to), + 'ref': 'cake', + }; + + final uri = Uri.https(apiAuthority, getRate, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map<String, dynamic>; + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final data = responseBody['data'] as Map<String, dynamic>; + double rate = double.parse(data['price'].toString()); + return rate; + } catch (e) { + print("error fetching rate: ${e.toString()}"); + return 0.0; + } + } + + @override + Future<Trade> createTrade({ + required TradeRequest request, + required bool isFixedRateMode, + required bool isSendAll, + }) async { + try { + final headers = <String, String>{}; + final params = <String, dynamic>{}; + var body = <String, dynamic>{ + 'coin_send': _normalizeCurrency(request.fromCurrency), + 'coin_receive': _normalizeCurrency(request.toCurrency), + 'amount_send': request.fromAmount, + 'recipient': request.toAddress, + 'ref': 'cake', + 'markup': markup, + }; + + String? fromNetwork = _networkFor(request.fromCurrency); + String? toNetwork = _networkFor(request.toCurrency); + if (fromNetwork != null) body['coin_send_network'] = fromNetwork; + if (toNetwork != null) body['coin_receive_network'] = toNetwork; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map<String, dynamic>; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map<String, dynamic>; + + return Trade( + id: responseData["order_id"] as String, + inputAddress: responseData["server_address"] as String, + amount: request.fromAmount, + from: request.fromCurrency, + to: request.toCurrency, + provider: description, + createdAt: DateTime.now(), + state: TradeState.created, + payoutAddress: request.toAddress, + isSendAll: isSendAll, + ); + } catch (e) { + print("error creating trade: ${e.toString()}"); + throw TradeNotCreatedException(description, description: e.toString()); + } + } + + @override + Future<Trade> findTradeById({required String id}) async { + try { + final headers = <String, String>{}; + final params = <String, dynamic>{}; + var body = <String, dynamic>{ + 'order_id': id, + }; + + final uri = Uri.https(apiAuthority, createOrder, params); + final response = await post(uri, body: body, headers: headers); + final responseBody = json.decode(response.body) as Map<String, dynamic>; + + if (response.statusCode == 400 || responseBody["success"] == false) { + final error = responseBody['errors'][0]['msg'] as String; + throw TradeNotCreatedException(description, description: error); + } + + if (response.statusCode != 200) + throw Exception('Unexpected http status: ${response.statusCode}'); + + final responseData = responseBody['data'] as Map<String, dynamic>; + final fromCurrency = responseData['coin_send'] as String; + final from = CryptoCurrency.fromString(fromCurrency); + final toCurrency = responseData['coin_receive'] as String; + final to = CryptoCurrency.fromString(toCurrency); + final inputAddress = responseData['server_address'] as String; + final status = responseData['status'] as String; + final state = TradeState.deserialize(raw: status); + final response_id = responseData['order_id'] as String; + final expectedSendAmount = responseData['amount_send'] as String; + + return Trade( + id: response_id, + from: from, + to: to, + provider: description, + inputAddress: inputAddress, + amount: expectedSendAmount, + state: state, + ); + } catch (e) { + print("error getting trade: ${e.toString()}"); + throw TradeNotFoundException( + id, + provider: description, + description: e.toString(), + ); + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + switch (currency) { + default: + return currency.title.toUpperCase(); + } + } + + String? _networkFor(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.usdt: + return "USDT_ERC20"; + case CryptoCurrency.bnb: + return "BNB_BSC"; + default: + return null; + } + } +} diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index 6cc3fddbe..aeb544ece 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -5,9 +5,6 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:hive/hive.dart'; -part 'trade.g.dart'; - -@HiveType(typeId: Trade.typeId) class Trade extends HiveObject { Trade({ required this.id, @@ -32,6 +29,7 @@ class Trade extends HiveObject { this.txId, this.isRefund, this.isSendAll, + this.router, }) { if (provider != null) providerRaw = provider.raw; @@ -121,21 +119,26 @@ class Trade extends HiveObject { @HiveField(21) bool? isSendAll; + @HiveField(22) + String? router; + static Trade fromMap(Map<String, Object?> map) { return Trade( - id: map['id'] as String, - provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), - from: CryptoCurrency.deserialize(raw: map['input'] as int), - to: CryptoCurrency.deserialize(raw: map['output'] as int), - createdAt: - map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, - amount: map['amount'] as String, - walletId: map['wallet_id'] as String, - fromWalletAddress: map['from_wallet_address'] as String?, - memo: map['memo'] as String?, - txId: map['tx_id'] as String?, - isRefund: map['isRefund'] as bool?, - isSendAll: map['isSendAll'] as bool?); + id: map['id'] as String, + provider: ExchangeProviderDescription.deserialize(raw: map['provider'] as int), + from: CryptoCurrency.deserialize(raw: map['input'] as int), + to: CryptoCurrency.deserialize(raw: map['output'] as int), + createdAt: + map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, + amount: map['amount'] as String, + walletId: map['wallet_id'] as String, + fromWalletAddress: map['from_wallet_address'] as String?, + memo: map['memo'] as String?, + txId: map['tx_id'] as String?, + isRefund: map['isRefund'] as bool?, + isSendAll: map['isSendAll'] as bool?, + router: map['router'] as String?, + ); } Map<String, dynamic> toMap() { @@ -152,8 +155,111 @@ class Trade extends HiveObject { 'tx_id': txId, 'isRefund': isRefund, 'isSendAll': isSendAll, + 'router': router, }; } String amountFormatted() => formatAmount(amount); } + +class TradeAdapter extends TypeAdapter<Trade> { + @override + final int typeId = Trade.typeId; + + @override + Trade read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = <int, dynamic>{}; + for (int i = 0; i < numOfFields; i++) { + try { + fields[reader.readByte()] = reader.read(); + } catch (_) {} + } + + return Trade( + id: fields[0] == null ? '' : fields[0] as String, + amount: fields[7] == null ? '' : fields[7] as String, + createdAt: fields[5] as DateTime?, + expiredAt: fields[6] as DateTime?, + inputAddress: fields[8] as String?, + extraId: fields[9] as String?, + outputTransaction: fields[10] as String?, + refundAddress: fields[11] as String?, + walletId: fields[12] as String?, + payoutAddress: fields[13] as String?, + password: fields[14] as String?, + providerId: fields[15] as String?, + providerName: fields[16] as String?, + fromWalletAddress: fields[17] as String?, + memo: fields[18] as String?, + txId: fields[19] as String?, + isRefund: fields[20] as bool?, + isSendAll: fields[21] as bool?, + router: fields[22] as String?, + ) + ..providerRaw = fields[1] == null ? 0 : fields[1] as int + ..fromRaw = fields[2] == null ? 0 : fields[2] as int + ..toRaw = fields[3] == null ? 0 : fields[3] as int + ..stateRaw = fields[4] == null ? '' : fields[4] as String; + } + + @override + void write(BinaryWriter writer, Trade obj) { + writer + ..writeByte(23) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.providerRaw) + ..writeByte(2) + ..write(obj.fromRaw) + ..writeByte(3) + ..write(obj.toRaw) + ..writeByte(4) + ..write(obj.stateRaw) + ..writeByte(5) + ..write(obj.createdAt) + ..writeByte(6) + ..write(obj.expiredAt) + ..writeByte(7) + ..write(obj.amount) + ..writeByte(8) + ..write(obj.inputAddress) + ..writeByte(9) + ..write(obj.extraId) + ..writeByte(10) + ..write(obj.outputTransaction) + ..writeByte(11) + ..write(obj.refundAddress) + ..writeByte(12) + ..write(obj.walletId) + ..writeByte(13) + ..write(obj.payoutAddress) + ..writeByte(14) + ..write(obj.password) + ..writeByte(15) + ..write(obj.providerId) + ..writeByte(16) + ..write(obj.providerName) + ..writeByte(17) + ..write(obj.fromWalletAddress) + ..writeByte(18) + ..write(obj.memo) + ..writeByte(19) + ..write(obj.txId) + ..writeByte(20) + ..write(obj.isRefund) + ..writeByte(21) + ..write(obj.isSendAll) + ..writeByte(22) + ..write(obj.router); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TradeAdapter && runtimeType == other.runtimeType && typeId == other.typeId; +} diff --git a/lib/exchange/trade_state.dart b/lib/exchange/trade_state.dart index 2c58a96f4..0a196835e 100644 --- a/lib/exchange/trade_state.dart +++ b/lib/exchange/trade_state.dart @@ -28,6 +28,7 @@ class TradeState extends EnumerableItem<String> with Serializable<String> { TradeState(raw: 'waitingAuthorization', title: 'Waiting authorization'); static const failed = TradeState(raw: 'failed', title: 'Failed'); static const completed = TradeState(raw: 'completed', title: 'Completed'); + static const expired = TradeState(raw: 'expired', title: 'Expired'); static const settling = TradeState(raw: 'settling', title: 'Settlement in progress'); static const settled = TradeState(raw: 'settled', title: 'Settlement completed'); static const wait = TradeState(raw: 'wait', title: 'Waiting'); @@ -39,7 +40,33 @@ class TradeState extends EnumerableItem<String> with Serializable<String> { static const exchanging = TradeState(raw: 'exchanging', title: 'Exchanging'); static const sending = TradeState(raw: 'sending', title: 'Sending'); static const success = TradeState(raw: 'success', title: 'Success'); + static TradeState deserialize({required String raw}) { + + switch (raw) { + case '1': + return unpaid; + case '2': + return paidUnconfirmed; + case '3': + return sending; + case '4': + return confirmed; + case '5': + case '6': + return exchanging; + case '7': + return sending; + case '8': + return complete; + case '9': + return expired; + case '10': + return underpaid; + case '11': + return failed; + } + switch (raw) { case 'NOT_FOUND': return notFound; diff --git a/lib/main.dart b/lib/main.dart index fa71da31d..eeee4fbc3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,6 @@ import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/entities/language_service.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/locales/locale.dart'; -import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; @@ -38,7 +37,6 @@ import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/exchange_template.dart'; import 'package:cake_wallet/src/screens/root/root.dart'; -import 'package:uni_links/uni_links.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/cake_hive.dart'; @@ -46,9 +44,10 @@ import 'package:cw_core/window_size.dart'; final navigatorKey = GlobalKey<NavigatorState>(); final rootKey = GlobalKey<RootState>(); -final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); +final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>(); Future<void> main() async { + bool isAppRunning = false; await runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); @@ -63,13 +62,42 @@ Future<void> main() async { }; await setDefaultMinimumWindowSize(); - + await CakeHive.close(); await initializeAppConfigs(); runApp(App()); + + isAppRunning = true; }, (error, stackTrace) async { + if (!isAppRunning) { + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: SingleChildScrollView( + child: Container( + margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20), + child: Column( + children: [ + Text( + 'Error:\n${error.toString()}', + style: TextStyle(fontSize: 22), + ), + Text( + 'Stack trace:\n${stackTrace.toString()}', + style: TextStyle(fontSize: 16), + ), + ], + ), + ), + ), + ), + ), + ); + } + ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace)); }); } @@ -229,61 +257,6 @@ class App extends StatefulWidget { } class AppState extends State<App> with SingleTickerProviderStateMixin { - AppState() : yatStore = getIt.get<YatStore>(); - - YatStore yatStore; - StreamSubscription? stream; - - @override - void initState() { - super.initState(); - //_handleIncomingLinks(); - //_handleInitialUri(); - } - - Future<void> _handleInitialUri() async { - try { - final uri = await getInitialUri(); - print('uri: $uri'); - if (uri == null) { - return; - } - if (!mounted) return; - //_fetchEmojiFromUri(uri); - } catch (e) { - if (!mounted) return; - print(e.toString()); - } - } - - void _handleIncomingLinks() { - if (!kIsWeb) { - stream = getUriLinksStream().listen((Uri? uri) { - print('uri: $uri'); - if (!mounted) return; - //_fetchEmojiFromUri(uri); - }, onError: (Object error) { - if (!mounted) return; - print('Error: $error'); - }); - } - } - - void _fetchEmojiFromUri(Uri uri) { - //final queryParameters = uri.queryParameters; - //if (queryParameters?.isEmpty ?? true) { - // return; - //} - //final emoji = queryParameters['eid']; - //final refreshToken = queryParameters['refresh_token']; - //if ((emoji?.isEmpty ?? true)||(refreshToken?.isEmpty ?? true)) { - // return; - //} - //yatStore.emoji = emoji; - //yatStore.refreshToken = refreshToken; - //yatStore.emojiIncommingSC.add(emoji); - } - @override Widget build(BuildContext context) { return Observer(builder: (BuildContext context) { diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index 454d124da..a703c9f9e 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -13,6 +15,9 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/restore/restore_from_qr_vm.dart'; import 'package:cake_wallet/view_model/restore/wallet_restore_from_qr_code.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -24,6 +29,19 @@ class RestoreOptionsPage extends BasePage { final bool isNewInstall; + bool get _doesSupportHardwareWallets { + if (!DeviceInfo.instance.isMobile) { + return false; + } + + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; + } + + return true; + } + @override Widget body(BuildContext context) { final imageColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor; @@ -57,7 +75,7 @@ class RestoreOptionsPage extends BasePage { description: S.of(context).restore_description_from_backup, ), ), - if (DeviceInfo.instance.isMobile) + if (_doesSupportHardwareWallets) Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index afdd14865..b6406dfbd 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -1,10 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/device_info.dart'; -import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:flutter/material.dart'; @@ -13,7 +10,6 @@ import 'package:cake_wallet/src/screens/auth/auth_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/entities/qr_scanner.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:mobx/mobx.dart'; import 'package:uni_links/uni_links.dart'; import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart'; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 94fec2fa2..c5ce7a591 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -48,6 +49,9 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.exolix: _provider = ExolixExchangeProvider(); break; + case ExchangeProviderDescription.quantex: + _provider = QuantexExchangeProvider(); + break; case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index e5533f48a..1560a4be0 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -30,6 +30,7 @@ import 'package:cake_wallet/exchange/limits_state.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -157,6 +158,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates), ThorChainExchangeProvider(tradesStore: trades), if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(), + QuantexExchangeProvider(), ]; @observable diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index 06ddaf275..f05b1c805 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,8 +1,12 @@ +import 'dart:io'; + import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/wallet_type_utils.dart'; +import 'package:cw_core/hardware/device_connection_type.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:ledger_flutter/ledger_flutter.dart'; @@ -11,8 +15,21 @@ import 'package:permission_handler/permission_handler.dart'; class LedgerViewModel { late final Ledger ledger; + bool get _doesSupportHardwareWallets { + if (!DeviceInfo.instance.isMobile) { + return false; + } + + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) + .isNotEmpty; + } + + return true; + } + LedgerViewModel() { - if (DeviceInfo.instance.isMobile) { + if (_doesSupportHardwareWallets) { ledger = Ledger( options: LedgerOptions( scanMode: ScanMode.balanced, diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 1da322778..c88008982 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -4,6 +4,7 @@ import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/exolix_exchange_provider.dart'; +import 'package:cake_wallet/exchange/provider/quantex_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; @@ -56,6 +57,9 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.thorChain: _provider = ThorChainExchangeProvider(tradesStore: trades); break; + case ExchangeProviderDescription.quantex: + _provider = QuantexExchangeProvider(); + break; } _updateItems(); @@ -80,6 +84,8 @@ abstract class TradeDetailsViewModelBase with Store { return 'https://exolix.com/transaction/${trade.id}'; case ExchangeProviderDescription.thorChain: return 'https://track.ninerealms.com/${trade.id}'; + case ExchangeProviderDescription.quantex: + return 'https://myquantex.com/send/${trade.id}'; } return null; } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index c671a013f..b520a3179 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=86 +MONERO_COM_VERSION="1.13.2" +MONERO_COM_BUILD_NUMBER=88 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=210 +CAKEWALLET_VERSION="4.16.2" +CAKEWALLET_BUILD_NUMBER=212 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index f7887e4bf..f70963745 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.13.0" -MONERO_COM_BUILD_NUMBER=84 +MONERO_COM_VERSION="1.13.2" +MONERO_COM_BUILD_NUMBER=86 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.16.0" -CAKEWALLET_BUILD_NUMBER=236 +CAKEWALLET_VERSION="4.16.2" +CAKEWALLET_BUILD_NUMBER=240 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index e16df1e61..afdac3e6c 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.3.0" -MONERO_COM_BUILD_NUMBER=17 +MONERO_COM_VERSION="1.3.2" +MONERO_COM_BUILD_NUMBER=19 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.9.0" -CAKEWALLET_BUILD_NUMBER=71 +CAKEWALLET_VERSION="1.9.2" +CAKEWALLET_BUILD_NUMBER=73 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 89e4de12d..9559e83b3 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -38,6 +38,7 @@ class SecretKey { SecretKey('walletConnectProjectId', () => ''), SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), + SecretKey('quantexExchangeMarkup', () => ''), ]; static final evmChainsSecrets = [