diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index cdf537cdb..53008ed52 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -111,6 +111,8 @@ jobs: echo "const anypayToken = '${{ secrets.ANY_PAY_TOKEN }}';" >> lib/.secrets.g.dart echo "const ioniaClientId = '${{ secrets.IONIA_CLIENT_ID }}';" >> lib/.secrets.g.dart echo "const twitterBearerToken = '${{ secrets.TWITTER_BEARER_TOKEN }}';" >> lib/.secrets.g.dart + echo "const trocadorApiKey = '${{ secrets.TROCADOR_API_KEY }}';" >> lib/.secrets.g.dart + echo "const trocadorExchangeMarkup = '${{ secrets.TROCADOR_EXCHANGE_MARKUP }}';" >> lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties diff --git a/assets/images/trocador.png b/assets/images/trocador.png new file mode 100644 index 000000000..67c9f221c Binary files /dev/null and b/assets/images/trocador.png differ diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 00bfc237c..925f39de0 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -117,7 +117,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const xusd = CryptoCurrency(title: 'XUSD', tag: 'XHV', raw: 29, name: 'xusd'); static const ape = CryptoCurrency(title: 'APE', tag: 'ETH', fullName: 'ApeCoin', raw: 30, name: 'ape', iconPath: 'assets/images/ape_icon.png'); - static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'C-CHAIN', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png'); + static const avaxc = CryptoCurrency(title: 'AVAX', tag: 'AVAXC', raw: 31, name: 'avaxc', iconPath: 'assets/images/avaxc_icon.png'); static const btt = CryptoCurrency(title: 'BTT', tag: 'ETH', fullName: 'BitTorrent', raw: 32, name: 'btt', iconPath: 'assets/images/btt_icon.png'); static const bttc = CryptoCurrency(title: 'BTTC', tag: 'TRX', fullName: 'BitTorrent-NEW', raw: 33, name: 'bttc', iconPath: 'assets/images/bttbsc_icon.png'); static const doge = CryptoCurrency(title: 'DOGE', fullName: 'Dogecoin', raw: 34, name: 'doge', iconPath: 'assets/images/doge_icon.png'); diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 50503361c..e33aeb803 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: flutter: sdk: flutter http: ^0.13.4 + file: ^6.1.4 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index ffcb9eb4c..3cb434efe 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -217,7 +217,7 @@ class BackupService { final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; - final disableExchange = data[PreferencesKey.disableExchangeKey] as bool?; + final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; @@ -280,9 +280,9 @@ class BackupService { await _sharedPreferences.setInt( PreferencesKey.currentTheme, currentTheme); - if (disableExchange != null) - await _sharedPreferences.setBool( - PreferencesKey.disableExchangeKey, disableExchange); + if (exchangeStatus != null) + await _sharedPreferences.setInt( + PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) await _sharedPreferences.setInt( @@ -431,8 +431,8 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.disableExchangeKey: - _sharedPreferences.getBool(PreferencesKey.disableExchangeKey), + PreferencesKey.exchangeStatusKey: + _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 3c8d9fbbe..94fc7ede8 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,10 +1,12 @@ import 'dart:io' show File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; @@ -142,7 +144,9 @@ Future defaultSettingsMigration( case 19: await validateBitcoinSavedTransactionPriority(sharedPreferences); break; - + case 20: + await migrateExchangeStatus(sharedPreferences); + break; default: break; } @@ -501,3 +505,15 @@ Future changeDefaultHavenNode( await node.save(); }); } + +Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { + final isExchangeDisabled = sharedPreferences.getBool(PreferencesKey.disableExchangeKey); + if (isExchangeDisabled == null) { + return; + } + + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled + ? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw); + + await sharedPreferences.remove(PreferencesKey.disableExchangeKey); +} diff --git a/lib/entities/exchange_api_mode.dart b/lib/entities/exchange_api_mode.dart new file mode 100644 index 000000000..0b8b575a5 --- /dev/null +++ b/lib/entities/exchange_api_mode.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/enumerable_item.dart'; + +class ExchangeApiMode extends EnumerableItem with Serializable { + const ExchangeApiMode({required String title, required int raw}) : super(title: title, raw: raw); + + static const all = [ExchangeApiMode.enabled, ExchangeApiMode.torOnly, ExchangeApiMode.disabled]; + + static const enabled = ExchangeApiMode(raw: 0, title: 'Enabled'); + static const torOnly = ExchangeApiMode(raw: 1, title: 'Tor only'); + static const disabled = ExchangeApiMode(raw: 2, title: 'Disabled'); + + static ExchangeApiMode deserialize({required int raw}) { + switch (raw) { + case 0: + return enabled; + case 1: + return torOnly; + case 2: + return disabled; + default: + throw Exception('Unexpected token: $raw for ExchangeApiMode deserialize'); + } + } + + @override + String toString() { + switch (this) { + case ExchangeApiMode.enabled: + return S.current.enabled; + case ExchangeApiMode.torOnly: + return S.current.tor_only; + case ExchangeApiMode.disabled: + return S.current.disabled; + default: + return ''; + } + } +} \ No newline at end of file diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index a44dcb3d9..e237b0691 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -13,6 +13,7 @@ class PreferencesKey { static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; static const disableExchangeKey = 'disable_exchange'; + static const exchangeStatusKey = 'exchange_status'; static const currentTheme = 'current_theme'; static const isDarkThemeLegacy = 'dark_theme'; static const displayActionListModeKey = 'display_list_mode'; diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index 3f18d43b1..e173dbdf6 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -269,6 +269,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { : currency.title.toLowerCase(); } } + } String normalizeCryptoCurrency(CryptoCurrency currency) { diff --git a/lib/exchange/exchange_provider.dart b/lib/exchange/exchange_provider.dart index 763350c94..cc81a21f6 100644 --- a/lib/exchange/exchange_provider.dart +++ b/lib/exchange/exchange_provider.dart @@ -14,6 +14,7 @@ abstract class ExchangeProvider { bool get isAvailable; bool get isEnabled; bool get supportsFixedRate; + bool get supportsOnionAddress => false; @override String toString() => title; diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 2fd231085..e545f69ce 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -1,31 +1,30 @@ import 'package:cw_core/enumerable_item.dart'; -class ExchangeProviderDescription extends EnumerableItem - with Serializable { - const ExchangeProviderDescription({ - required String title, - required int raw, - required this.image, - this.horizontalLogo = false}) +class ExchangeProviderDescription extends EnumerableItem with Serializable { + const ExchangeProviderDescription( + {required String title, required int raw, required this.image, this.horizontalLogo = false}) : super(title: title, raw: raw); final bool horizontalLogo; final String image; - static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png'); + static const xmrto = + ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png'); static const changeNow = ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png'); static const morphToken = ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png'); - static const sideShift = + static const sideShift = ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png'); - static const simpleSwap = - ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); + static const simpleSwap = ExchangeProviderDescription( + title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png'); - static const all = - ExchangeProviderDescription(title: 'All trades', raw: 5, image:''); + static const trocador = + ExchangeProviderDescription(title: 'Trocador', raw: 5, image: 'assets/images/trocador.png'); + + static const all = ExchangeProviderDescription(title: 'All trades', raw: 6, image: ''); static ExchangeProviderDescription deserialize({required int raw}) { switch (raw) { @@ -40,6 +39,8 @@ class ExchangeProviderDescription extends EnumerableItem case 4: return simpleSwap; case 5: + return trocador; + case 6: return all; default: throw Exception('Unexpected token: $raw for ExchangeProviderDescription deserialize'); diff --git a/lib/exchange/trade.dart b/lib/exchange/trade.dart index f20a080b7..70dfa5713 100644 --- a/lib/exchange/trade.dart +++ b/lib/exchange/trade.dart @@ -8,21 +8,25 @@ part 'trade.g.dart'; @HiveType(typeId: Trade.typeId) class Trade extends HiveObject { - Trade( - {required this.id, - required this.amount, - ExchangeProviderDescription? provider, - CryptoCurrency? from, - CryptoCurrency? to, - TradeState? state, - this.createdAt, - this.expiredAt, - this.inputAddress, - this.extraId, - this.outputTransaction, - this.refundAddress, - this.walletId, - this.payoutAddress}) { + Trade({ + required this.id, + required this.amount, + ExchangeProviderDescription? provider, + CryptoCurrency? from, + CryptoCurrency? to, + TradeState? state, + this.createdAt, + this.expiredAt, + this.inputAddress, + this.extraId, + this.outputTransaction, + this.refundAddress, + this.walletId, + this.payoutAddress, + this.password, + this.providerId, + this.providerName, + }) { if (provider != null) { providerRaw = provider.raw; } @@ -92,16 +96,23 @@ class Trade extends HiveObject { @HiveField(13) String? payoutAddress; + @HiveField(14) + String? password; + + @HiveField(15) + String? providerId; + + @HiveField(16) + String? providerName; + static Trade fromMap(Map map) { return Trade( id: map['id'] as String, - provider: ExchangeProviderDescription.deserialize( - raw: map['provider'] as int), + 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, + createdAt: + map['date'] != null ? DateTime.fromMillisecondsSinceEpoch(map['date'] as int) : null, amount: map['amount'] as String, walletId: map['wallet_id'] as String); } diff --git a/lib/exchange/trocador/trocador_exchange_provider.dart b/lib/exchange/trocador/trocador_exchange_provider.dart new file mode 100644 index 000000000..98726a265 --- /dev/null +++ b/lib/exchange/trocador/trocador_exchange_provider.dart @@ -0,0 +1,312 @@ +import 'dart:convert'; + +import 'package:cake_wallet/exchange/exchange_pair.dart'; +import 'package:cake_wallet/exchange/exchange_provider.dart'; +import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/limits.dart'; +import 'package:cake_wallet/exchange/exchange_provider_description.dart'; +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:http/http.dart'; + +class TrocadorExchangeProvider extends ExchangeProvider { + TrocadorExchangeProvider({this.useTorOnly = false}) + : _lastUsedRateId = '', + super(pairList: _supportedPairs()); + + bool useTorOnly; + + static const List _notSupported = [ + CryptoCurrency.scrt, + CryptoCurrency.stx, + CryptoCurrency.zaddr, + ]; + + static List _supportedPairs() { + final supportedCurrencies = + CryptoCurrency.all.where((element) => !_notSupported.contains(element)).toList(); + + return supportedCurrencies + .map((i) => supportedCurrencies.map((k) => ExchangePair(from: i, to: k, reverse: true))) + .expand((i) => i) + .toList(); + } + + static const onionApiAuthority = 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; + static const clearNetAuthority = 'trocador.app'; + static const apiKey = secrets.trocadorApiKey; + static const markup = secrets.trocadorExchangeMarkup; + static const newRatePath = '/api/new_rate'; + static const createTradePath = 'api/new_trade'; + static const tradePath = 'api/trade'; + static const coinPath = 'api/coin'; + String _lastUsedRateId; + + @override + Future checkIsAvailable() async => true; + + @override + Future createTrade({required TradeRequest request, required bool isFixedRateMode}) { + final _request = request as TrocadorRequest; + return _createTrade(request: _request, isFixedRateMode: isFixedRateMode); + } + + Future _createTrade({ + required TrocadorRequest request, + required bool isFixedRateMode, + }) async { + final params = { + 'api_key': apiKey, + 'ticker_from': request.from.title.toLowerCase(), + 'ticker_to': request.to.title.toLowerCase(), + 'network_from': _networkFor(request.from), + 'network_to': _networkFor(request.to), + 'payment': isFixedRateMode ? 'True' : 'False', + 'min_kycrating': 'C', + 'markup': markup, + 'best_only': 'True', + if (!isFixedRateMode) 'amount_from': request.fromAmount, + if (isFixedRateMode) 'amount_to': request.toAmount, + 'address': request.address, + 'refund': request.refundAddress + }; + + if (isFixedRateMode) { + await fetchRate( + from: request.from, + to: request.to, + amount: double.tryParse(request.toAmount) ?? 0, + isFixedRateMode: true, + isReceiveAmount: true, + ); + params['id'] = _lastUsedRateId; + } + + final uri = await _getUri(createTradePath, params); + final response = await get(uri); + + if (response.statusCode == 400) { + final responseJSON = json.decode(response.body) as Map; + final error = responseJSON['error'] as String; + final message = responseJSON['message'] as String; + throw Exception('${error}\n$message'); + } + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final responseJSON = json.decode(response.body) as Map; + final id = responseJSON['trade_id'] as String; + final inputAddress = responseJSON['address_provider'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final status = responseJSON['status'] as String; + final state = TradeState.deserialize(raw: status); + final payoutAddress = responseJSON['address_user'] as String; + final date = responseJSON['date'] as String; + final password = responseJSON['password'] as String; + final providerId = responseJSON['id_provider'] as String; + final providerName = responseJSON['provider'] as String; + + return Trade( + id: id, + from: request.from, + to: request.to, + provider: description, + inputAddress: inputAddress, + refundAddress: refundAddress, + state: state, + password: password, + providerId: providerId, + providerName: providerName, + createdAt: DateTime.tryParse(date)?.toLocal(), + amount: responseJSON['amount_from']?.toString() ?? request.fromAmount, + payoutAddress: payoutAddress); + } + + @override + ExchangeProviderDescription get description => ExchangeProviderDescription.trocador; + + @override + Future fetchLimits( + {required CryptoCurrency from, + required CryptoCurrency to, + required bool isFixedRateMode}) async { + final params = { + 'api_key': apiKey, + 'ticker': from.title.toLowerCase(), + 'name': from.name, + }; + + final uri = await _getUri(coinPath, params); + + final response = await get(uri); + + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final responseJSON = json.decode(response.body) as List; + + if (responseJSON.isEmpty) { + throw Exception('No data'); + } + + final coinJson = responseJSON.first as Map; + + return Limits( + min: coinJson['minimum'] as double, + max: coinJson['maximum'] as double, + ); + } + + @override + Future 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 params = { + 'api_key': apiKey, + 'ticker_from': from.title.toLowerCase(), + 'ticker_to': to.title.toLowerCase(), + 'network_from': _networkFor(from), + 'network_to': _networkFor(to), + if (!isFixedRateMode) 'amount_from': amount.toString(), + if (isFixedRateMode) 'amount_to': amount.toString(), + 'payment': isFixedRateMode ? 'True' : 'False', + 'min_kycrating': 'C', + 'markup': markup, + 'best_only': 'True', + }; + + final uri = await _getUri(newRatePath, params); + final response = await get(uri); + final responseJSON = json.decode(response.body) as Map; + final fromAmount = double.parse(responseJSON['amount_from'].toString()); + final toAmount = double.parse(responseJSON['amount_to'].toString()); + final rateId = responseJSON['trade_id'] as String? ?? ''; + + if (rateId.isNotEmpty) { + _lastUsedRateId = rateId; + } + + return isReceiveAmount ? (amount / fromAmount) : (toAmount / amount); + } catch (e) { + print(e.toString()); + return 0.0; + } + } + + @override + Future findTradeById({required String id}) async { + final uri = await _getUri(tradePath, {'api_key': apiKey, 'id': id}); + return get(uri).then((response) { + if (response.statusCode != 200) { + throw Exception('Unexpected http status: ${response.statusCode}'); + } + + final responseListJson = json.decode(response.body) as List; + + final responseJSON = responseListJson.first; + final id = responseJSON['trade_id'] as String; + final inputAddress = responseJSON['address_user'] as String; + final refundAddress = responseJSON['refund_address'] as String; + final payoutAddress = responseJSON['address_provider'] as String; + final fromAmount = responseJSON['amount_from']?.toString() ?? '0'; + final from = CryptoCurrency.fromString(responseJSON['ticker_from'] as String); + final to = CryptoCurrency.fromString(responseJSON['ticker_to'] as String); + final state = TradeState.deserialize(raw: responseJSON['status'] as String); + final date = DateTime.parse(responseJSON['date'] as String); + final password = responseJSON['password'] as String; + final providerId = responseJSON['id_provider'] as String; + final providerName = responseJSON['provider'] as String; + + return Trade( + id: id, + from: from, + to: to, + provider: description, + inputAddress: inputAddress, + refundAddress: refundAddress, + createdAt: date, + amount: fromAmount, + state: state, + payoutAddress: payoutAddress, + password: password, + providerId: providerId, + providerName: providerName, + ); + }); + } + + @override + bool get isAvailable => true; + + @override + bool get isEnabled => true; + + @override + bool get supportsFixedRate => true; + + @override + bool get supportsOnionAddress => true; + + @override + String get title => 'Trocador'; + + String _networkFor(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.eth: + return 'ERC20'; + case CryptoCurrency.maticpoly: + return 'Mainnet'; + case CryptoCurrency.usdcpoly: + return 'MATIC'; + case CryptoCurrency.zec: + return 'Mainnet'; + default: + return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet'; + } + } + + String _normalizeTag(String tag) { + switch (tag) { + case 'ETH': + return 'ERC20'; + case 'TRX': + return 'TRC20'; + default: + return tag.toLowerCase(); + } + } + + Future _getUri(String path, Map queryParams) async { + if (!supportsOnionAddress) { + return Uri.https(clearNetAuthority, path, queryParams); + } + + final uri = Uri.http(onionApiAuthority, path, queryParams); + + if (useTorOnly) { + return uri; + } + + try { + await get(uri); + + return uri; + } catch (e) { + return Uri.https(clearNetAuthority, path, queryParams); + } + } +} diff --git a/lib/exchange/trocador/trocador_request.dart b/lib/exchange/trocador/trocador_request.dart new file mode 100644 index 000000000..fbb8fdc84 --- /dev/null +++ b/lib/exchange/trocador/trocador_request.dart @@ -0,0 +1,21 @@ +import 'package:cake_wallet/exchange/trade_request.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class TrocadorRequest extends TradeRequest { + TrocadorRequest( + {required this.from, + required this.to, + required this.address, + required this.fromAmount, + required this.toAmount, + required this.refundAddress, + required this.isReverse}); + + CryptoCurrency from; + CryptoCurrency to; + String address; + String fromAmount; + String toAmount; + String refundAddress; + bool isReverse; +} diff --git a/lib/src/screens/dashboard/widgets/trade_row.dart b/lib/src/screens/dashboard/widgets/trade_row.dart index 21952140e..3b613b0e2 100644 --- a/lib/src/screens/dashboard/widgets/trade_row.dart +++ b/lib/src/screens/dashboard/widgets/trade_row.dart @@ -9,7 +9,8 @@ class TradeRow extends StatelessWidget { required this.to, required this.createdAtFormattedDate, this.onTap, - this.formattedAmount,}); + this.formattedAmount, + }); final VoidCallback? onTap; final ExchangeProviderDescription provider; @@ -35,47 +36,40 @@ class TradeRow extends StatelessWidget { SizedBox(width: 12), Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('${from.toString()} → ${to.toString()}', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor! - )), - formattedAmount != null - ? Text(formattedAmount! + ' ' + amountCrypto, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor! - )) - : Container() - ]), - SizedBox(height: 5), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (createdAtFormattedDate != null) - Text(createdAtFormattedDate!, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).textTheme! - .overline!.backgroundColor!)) - ]) - ], - ) - ) + mainAxisSize: MainAxisSize.min, + children: [ + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text('${from.toString()} → ${to.toString()}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)), + formattedAmount != null + ? Text(formattedAmount! + ' ' + amountCrypto, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: + Theme.of(context).accentTextTheme!.headline2!.backgroundColor!)) + : Container() + ]), + SizedBox(height: 5), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + if (createdAtFormattedDate != null) + Text(createdAtFormattedDate!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).textTheme!.overline!.backgroundColor!)) + ]) + ], + )) ], ), )); } - Image? _getPoweredImage(ExchangeProviderDescription provider) { - Image? image; + Widget? _getPoweredImage(ExchangeProviderDescription provider) { + Widget? image; switch (provider) { case ExchangeProviderDescription.xmrto: @@ -93,10 +87,15 @@ class TradeRow extends StatelessWidget { case ExchangeProviderDescription.simpleSwap: image = Image.asset('assets/images/simpleSwap.png', width: 36, height: 36); break; + case ExchangeProviderDescription.trocador: + image = ClipRRect( + borderRadius: BorderRadius.circular(50), + child: Image.asset('assets/images/trocador.png', width: 36, height: 36)); + break; default: image = null; } return image; } -} \ No newline at end of file +} diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index 6c2f996df..a82ddaf4e 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -1,7 +1,11 @@ +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/src/screens/nodes/widgets/node_form.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -48,29 +52,51 @@ class _AdvancedPrivacySettingsBodyState extends State Observer( - builder: (_) => SettingsSwitcherCell( - title: item.title, - value: item.value(), - onValueChange: item.onValueChange, - ), - ), + Observer( + builder: (_) { + return SettingsSwitcherCell( + title: S.current.disable_fiat, + value: widget.privacySettingsViewModel.fiatApi == FiatApiMode.disabled, + onValueChange: (BuildContext context, bool value) { + widget.privacySettingsViewModel.setFiatMode(value); + }); + } ), Observer( builder: (_) { - if (widget.privacySettingsViewModel.addCustomNode) { - return Padding( - padding: EdgeInsets.only(left: 24, right: 24, top: 24), - child: NodeForm( - formKey: _formKey, - nodeViewModel: widget.nodeViewModel, - ), - ); - } - return const SizedBox(); - }, + return SettingsChoicesCell( + ChoicesListItem( + title: S.current.exchange, + items: ExchangeApiMode.all, + selectedItem: widget.privacySettingsViewModel.exchangeStatus, + onItemSelected: (ExchangeApiMode mode) => + widget.privacySettingsViewModel.setExchangeApiMode(mode), + ), + ); + } ), + Observer( + builder: (_) { + return Column( + children: [ + SettingsSwitcherCell( + title: S.current.add_custom_node, + value: widget.privacySettingsViewModel.addCustomNode, + onValueChange: (_, __) => widget.privacySettingsViewModel.toggleAddCustomNode(), + ), + if (widget.privacySettingsViewModel.addCustomNode) + Padding( + padding: EdgeInsets.only(left: 24, right: 24, top: 24), + child: NodeForm( + formKey: _formKey, + nodeViewModel: widget.nodeViewModel, + ), + ) + ], + ); + } + ), + ], ), bottomSectionPadding: EdgeInsets.all(24), diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 2f15cc225..5322c488f 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:cake_wallet/view_model/settings/privacy_settings_view_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -27,12 +31,14 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext context, bool value) { _privacySettingsViewModel.setFiatMode(value); }), - SettingsSwitcherCell( - title: S.current.disable_exchange, - value: _privacySettingsViewModel.disableExchange, - onValueChange: (BuildContext context, bool value) { - _privacySettingsViewModel.setEnableExchange(value); - }), + SettingsChoicesCell( + ChoicesListItem( + title: S.current.exchange, + items: ExchangeApiMode.all, + selectedItem: _privacySettingsViewModel.exchangeStatus, + onItemSelected: (ExchangeApiMode mode) => _privacySettingsViewModel.setExchangeApiMode(mode), + ), + ), SettingsSwitcherCell( title: S.current.settings_save_recipient_address, value: _privacySettingsViewModel.shouldSaveRecipientAddress, diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart index e2f817fc4..80461e26d 100644 --- a/lib/src/widgets/check_box_picker.dart +++ b/lib/src/widgets/check_box_picker.dart @@ -38,7 +38,7 @@ class CheckBoxPickerState extends State { Column( mainAxisSize: MainAxisSize.min, children: [ - if (widget.title?.isNotEmpty ?? false) + if (widget.title.isNotEmpty) Container( padding: EdgeInsets.symmetric(horizontal: 24), child: Text( @@ -58,7 +58,7 @@ class CheckBoxPickerState extends State { child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(30)), child: Container( - color: Theme.of(context).accentTextTheme!.headline6!.color!, + color: Theme.of(context).accentTextTheme.headline6!.color!, child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.65, @@ -70,7 +70,7 @@ class CheckBoxPickerState extends State { child: Stack( alignment: Alignment.center, children: [ - (items?.length ?? 0) > 3 + (items.length) > 3 ? Scrollbar( controller: controller, child: itemsList(), @@ -95,14 +95,14 @@ class CheckBoxPickerState extends State { Widget itemsList() { return Container( - color: Theme.of(context).accentTextTheme!.headline6!.backgroundColor!, + color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, child: ListView.separated( padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, separatorBuilder: (context, index) => widget.isSeparated ? Divider( - color: Theme.of(context).accentTextTheme!.headline6!.backgroundColor!, + color: Theme.of(context).accentTextTheme.headline6!.backgroundColor!, height: 1, ) : const SizedBox(), @@ -121,13 +121,13 @@ class CheckBoxPickerState extends State { }, child: Container( height: 55, - color: Theme.of(context).accentTextTheme!.headline6!.color!, + color: Theme.of(context).accentTextTheme.headline6!.color!, padding: EdgeInsets.only(left: 24, right: 24), child: CheckboxListTile( value: item.value, activeColor: item.value ? Palette.blueCraiola - : Theme.of(context).accentTextTheme!.subtitle1!.decorationColor!, + : Theme.of(context).accentTextTheme.subtitle1!.decorationColor!, checkColor: Colors.white, title: widget.displayItem?.call(item) ?? Text( @@ -138,7 +138,7 @@ class CheckBoxPickerState extends State { fontWeight: FontWeight.w600, color: item.isDisabled ? Colors.grey.withOpacity(0.5) - : Theme.of(context).primaryTextTheme!.headline6!.color!, + : Theme.of(context).primaryTextTheme.headline6!.color!, decoration: TextDecoration.none, ), ), diff --git a/lib/store/dashboard/trade_filter_store.dart b/lib/store/dashboard/trade_filter_store.dart index 87fa749a9..c772a35d6 100644 --- a/lib/store/dashboard/trade_filter_store.dart +++ b/lib/store/dashboard/trade_filter_store.dart @@ -12,7 +12,8 @@ abstract class TradeFilterStoreBase with Store { displayChangeNow = true, displaySideShift = true, displayMorphToken = true, - displaySimpleSwap = true; + displaySimpleSwap = true, + displayTrocador = true; @observable bool displayXMRTO; @@ -29,8 +30,11 @@ abstract class TradeFilterStoreBase with Store { @observable bool displaySimpleSwap; + @observable + bool displayTrocador; + @computed - bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap; + bool get displayAllTrades => displayChangeNow && displaySideShift && displaySimpleSwap && displayTrocador; @action void toggleDisplayExchange(ExchangeProviderDescription provider) { @@ -50,6 +54,9 @@ abstract class TradeFilterStoreBase with Store { case ExchangeProviderDescription.morphToken: displayMorphToken = !displayMorphToken; break; + case ExchangeProviderDescription.trocador: + displayTrocador = !displayTrocador; + break; case ExchangeProviderDescription.all: if (displayAllTrades) { displayChangeNow = false; @@ -57,12 +64,14 @@ abstract class TradeFilterStoreBase with Store { displayXMRTO = false; displayMorphToken = false; displaySimpleSwap = false; + displayTrocador = false; } else { displayChangeNow = true; displaySideShift = true; displayXMRTO = true; displayMorphToken = true; displaySimpleSwap = true; + displayTrocador = true; } break; } @@ -88,7 +97,8 @@ abstract class TradeFilterStoreBase with Store { ExchangeProviderDescription.morphToken) ||(displaySimpleSwap && item.trade.provider == - ExchangeProviderDescription.simpleSwap)) + ExchangeProviderDescription.simpleSwap) + ||(displayTrocador && item.trade.provider == ExchangeProviderDescription.trocador)) .toList() : _trades; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index b10e0d08d..b6e5a7549 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -31,7 +32,7 @@ abstract class SettingsStoreBase with Store { required bool initialSaveRecipientAddress, required FiatApiMode initialFiatMode, required bool initialAllowBiometricalAuthentication, - required bool initialExchangeEnabled, + required ExchangeApiMode initialExchangeStatus, required ThemeBase initialTheme, required int initialPinLength, required String initialLanguageCode, @@ -53,7 +54,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = initialSaveRecipientAddress, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, - disableExchange = initialExchangeEnabled, + exchangeStatus = initialExchangeStatus, currentTheme = initialTheme, pinCodeLength = initialPinLength, languageCode = initialLanguageCode, @@ -153,9 +154,9 @@ abstract class SettingsStoreBase with Store { PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); reaction( - (_) => disableExchange, - (bool disableExchange) => sharedPreferences.setBool( - PreferencesKey.disableExchangeKey, disableExchange)); + (_) => exchangeStatus, + (ExchangeApiMode mode) => sharedPreferences.setInt( + PreferencesKey.exchangeStatusKey, mode.serialize())); this .nodes @@ -192,7 +193,7 @@ abstract class SettingsStoreBase with Store { bool allowBiometricalAuthentication; @observable - bool disableExchange; + ExchangeApiMode exchangeStatus; @observable ThemeBase currentTheme; @@ -284,8 +285,9 @@ abstract class SettingsStoreBase with Store { final allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; - final disableExchange = sharedPreferences - .getBool(PreferencesKey.disableExchangeKey) ?? false; + final exchangeStatus = ExchangeApiMode.deserialize( + raw: sharedPreferences + .getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); final legacyTheme = (sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy) ?? false) ? ThemeType.dark.index @@ -354,7 +356,7 @@ abstract class SettingsStoreBase with Store { initialSaveRecipientAddress: shouldSaveRecipientAddress, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, - initialExchangeEnabled: disableExchange, + initialExchangeStatus: exchangeStatus, initialTheme: savedTheme, actionlistDisplayMode: actionListDisplayMode, initialPinLength: pinLength, @@ -400,7 +402,9 @@ abstract class SettingsStoreBase with Store { allowBiometricalAuthentication = sharedPreferences .getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; - disableExchange = sharedPreferences.getBool(PreferencesKey.disableExchangeKey) ?? disableExchange; + exchangeStatus = ExchangeApiMode.deserialize( + raw: sharedPreferences + .getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); final legacyTheme = (sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy) ?? false) ? ThemeType.dark.index diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index f800e3418..fad5fff34 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -1,9 +1,8 @@ +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/view_model/settings/switcher_list_item.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/generated/i18n.dart'; part 'advanced_privacy_settings_view_model.g.dart'; @@ -11,35 +10,19 @@ class AdvancedPrivacySettingsViewModel = AdvancedPrivacySettingsViewModelBase with _$AdvancedPrivacySettingsViewModel; abstract class AdvancedPrivacySettingsViewModelBase with Store { - AdvancedPrivacySettingsViewModelBase(this.type, this._settingsStore) - : _addCustomNode = false { - settings = [ - SwitcherListItem( - title: S.current.disable_fiat, - value: () => _settingsStore.fiatApiMode == FiatApiMode.disabled, - onValueChange: (_, bool value) => setFiatMode(value), - ), - SwitcherListItem( - title: S.current.disable_exchange, - value: () => _settingsStore.disableExchange, - onValueChange: (_, bool value) { - _settingsStore.disableExchange = value; - }, - ), - SwitcherListItem( - title: S.current.add_custom_node, - value: () => _addCustomNode, - onValueChange: (_, bool value) => _addCustomNode = value, - ), - ]; - } + AdvancedPrivacySettingsViewModelBase(this.type, this._settingsStore) : _addCustomNode = false; - late List settings; + @computed + ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; + + @computed + FiatApiMode get fiatApi => _settingsStore.fiatApiMode; @observable bool _addCustomNode = false; final WalletType type; + final SettingsStore _settingsStore; @computed @@ -53,4 +36,14 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { } _settingsStore.fiatApiMode = FiatApiMode.enabled; } + + @action + void setExchangeApiMode(ExchangeApiMode value) { + _settingsStore.exchangeStatus = value; + } + + @action + void toggleAddCustomNode() { + _addCustomNode = !_addCustomNode; + } } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 12bd21058..d41e51411 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; @@ -96,6 +98,11 @@ abstract class DashboardViewModelBase with Store { caption: ExchangeProviderDescription.simpleSwap.title, onChanged: () => tradeFilterStore .toggleDisplayExchange(ExchangeProviderDescription.simpleSwap)), + FilterItem( + value: () => tradeFilterStore.displayTrocador, + caption: ExchangeProviderDescription.trocador.title, + onChanged: () => tradeFilterStore + .toggleDisplayExchange(ExchangeProviderDescription.trocador)), ] }, subname = '', @@ -268,7 +275,7 @@ abstract class DashboardViewModelBase with Store { settingsStore.shouldShowYatPopup = shouldShow; @computed - bool get isEnabledExchangeAction => !settingsStore.disableExchange; + bool get isEnabledExchangeAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; @observable bool hasExchangeAction; diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 541b74396..94c874979 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; +import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/exchange/changenow/changenow_exchange_provider.dart'; @@ -46,6 +47,9 @@ abstract class ExchangeTradeViewModelBase with Store { case ExchangeProviderDescription.simpleSwap: _provider = SimpleSwapExchangeProvider(); break; + case ExchangeProviderDescription.trocador: + _provider = TrocadorExchangeProvider(); + break; } _updateItems(); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index d310e6278..2dd67cab7 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -2,11 +2,15 @@ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; +import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; +import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; +import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -53,6 +57,7 @@ abstract class ExchangeViewModelBase with Store { isDepositAddressEnabled = false, isReceiveAddressEnabled = false, isReceiveAmountEditable = false, + _useTorOnly = false, receiveCurrencies = [], depositCurrencies = [], limits = Limits(min: 0, max: 0), @@ -60,8 +65,10 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsInitialState(), receiveCurrency = wallet.currency, depositCurrency = wallet.currency, - providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()], + providerList = [], selectedProviders = ObservableList() { + _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; + _setProviders(); const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano]; const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano]; @@ -117,13 +124,20 @@ abstract class ExchangeViewModelBase with Store { _calculateBestRate(); }); } - + bool _useTorOnly; final WalletBase wallet; final Box trades; final ExchangeTemplateStore _exchangeTemplateStore; final TradesStore tradesStore; final SharedPreferences sharedPreferences; + List get _allProviders => [ + ChangeNowExchangeProvider(), + SideShiftExchangeProvider(), + SimpleSwapExchangeProvider(), + TrocadorExchangeProvider(useTorOnly: _useTorOnly), + ]; + @observable ExchangeProvider? provider; @@ -455,6 +469,18 @@ abstract class ExchangeViewModelBase with Store { amount = isFixedRateMode ? receiveAmount : depositAmount; } + if (provider is TrocadorExchangeProvider) { + request = TrocadorRequest( + from: depositCurrency, + to: receiveCurrency, + fromAmount: depositAmount.replaceAll(',', '.'), + toAmount: receiveAmount.replaceAll(',', '.'), + refundAddress: depositAddress, + address: receiveAddress, + isReverse: isFixedRateMode); + amount = isFixedRateMode ? receiveAmount : depositAmount; + } + amount = amount.replaceAll(',', '.'); if (limitsState is LimitsLoadedSuccessfully) { @@ -675,4 +701,12 @@ abstract class ExchangeViewModelBase with Store { break; } } + + void _setProviders(){ + if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) { + providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); + } else { + providerList = _allProviders; + } + } } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index f8c3e5b50..1d58fc323 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -12,7 +13,7 @@ abstract class PrivacySettingsViewModelBase with Store { final SettingsStore _settingsStore; @computed - bool get disableExchange => _settingsStore.disableExchange; + ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; @computed bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; @@ -24,7 +25,7 @@ abstract class PrivacySettingsViewModelBase with Store { void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; @action - void setEnableExchange(bool value) => _settingsStore.disableExchange = value; + void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value; @action void setFiatMode(bool value) { diff --git a/lib/view_model/trade_details_view_model.dart b/lib/view_model/trade_details_view_model.dart index 5a1f78774..3a6f1ec74 100644 --- a/lib/view_model/trade_details_view_model.dart +++ b/lib/view_model/trade_details_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dar import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/trade.dart'; +import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/xmrto/xmrto_exchange_provider.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; @@ -22,16 +23,15 @@ import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item. import 'package:url_launcher/url_launcher.dart'; part 'trade_details_view_model.g.dart'; -class TradeDetailsViewModel = TradeDetailsViewModelBase - with _$TradeDetailsViewModel; +class TradeDetailsViewModel = TradeDetailsViewModelBase with _$TradeDetailsViewModel; abstract class TradeDetailsViewModelBase with Store { TradeDetailsViewModelBase({ required Trade tradeForDetails, required this.trades, - required this.settingsStore}) - : items = ObservableList(), - trade = tradeForDetails { + required this.settingsStore, + }) : items = ObservableList(), + trade = tradeForDetails { switch (trade.provider) { case ExchangeProviderDescription.xmrto: _provider = XMRTOExchangeProvider(); @@ -45,9 +45,12 @@ abstract class TradeDetailsViewModelBase with Store { case ExchangeProviderDescription.sideShift: _provider = SideShiftExchangeProvider(); break; - case ExchangeProviderDescription.simpleSwap: + case ExchangeProviderDescription.simpleSwap: _provider = SimpleSwapExchangeProvider(); break; + case ExchangeProviderDescription.trocador: + _provider = TrocadorExchangeProvider(); + break; } items = ObservableList(); @@ -96,12 +99,7 @@ abstract class TradeDetailsViewModelBase with Store { items.clear(); items.add( - DetailsListStatusItem( - title: S.current.trade_details_state, - value: trade.state != null - ? trade.state.toString() - : S.current.trade_details_fetching) - ); + DetailsListStatusItem(title: S.current.trade_details_state, value: trade.state.toString())); items.add(TradeDetailsListCardItem.tradeDetails( id: trade.id, @@ -114,15 +112,11 @@ abstract class TradeDetailsViewModelBase with Store { }, )); - if (trade.provider != null) { - items.add(StandartListItem( - title: S.current.trade_details_provider, - value: trade.provider.toString())); - } + items.add(StandartListItem( + title: S.current.trade_details_provider, value: trade.provider.toString())); if (trade.provider == ExchangeProviderDescription.changeNow) { - final buildURL = - 'https://changenow.io/exchange/txs/${trade.id.toString()}'; + final buildURL = 'https://changenow.io/exchange/txs/${trade.id.toString()}'; items.add(TrackTradeListItem( title: 'Track', value: buildURL, @@ -133,14 +127,25 @@ abstract class TradeDetailsViewModelBase with Store { if (trade.provider == ExchangeProviderDescription.sideShift) { final buildURL = 'https://sideshift.ai/orders/${trade.id.toString()}'; - items.add(TrackTradeListItem( - title: 'Track', value: buildURL, onTap: () => launch(buildURL))); + items.add(TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => launch(buildURL))); } if (trade.provider == ExchangeProviderDescription.simpleSwap) { final buildURL = 'https://simpleswap.io/exchange?id=${trade.id.toString()}'; - items.add(TrackTradeListItem( - title: 'Track', value: buildURL, onTap: () => launch(buildURL))); + items.add(TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => launch(buildURL))); + } + + if (trade.provider == ExchangeProviderDescription.trocador) { + final buildURL = 'https://trocador.app/en/checkout/${trade.id.toString()}'; + items.add(TrackTradeListItem(title: 'Track', value: buildURL, onTap: () => launch(buildURL))); + + items.add(StandartListItem( + title: '${trade.providerName} ${S.current.id.toUpperCase()}', + value: trade.providerId ?? '')); + + if (trade.password != null && trade.password!.isNotEmpty) + items.add(StandartListItem( + title: '${trade.providerName} ${S.current.password}', value: trade.password ?? '')); } } } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index d0fa39bfc..c27bc5ff7 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -6,17 +6,12 @@ class SecretKey { static final base = [ SecretKey('salt', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), - SecretKey('keychainSalt', - () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), + SecretKey('keychainSalt', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), SecretKey('key', () => hex.encode(encrypt.Key.fromSecureRandom(16).bytes)), - SecretKey( - 'walletSalt', () => hex.encode(encrypt.Key.fromSecureRandom(4).bytes)), - SecretKey( - 'shortKey', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), - SecretKey( - 'backupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(8).bytes)), - SecretKey('backupKeychainSalt', - () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), + SecretKey('walletSalt', () => hex.encode(encrypt.Key.fromSecureRandom(4).bytes)), + SecretKey('shortKey', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), + SecretKey('backupSalt', () => hex.encode(encrypt.Key.fromSecureRandom(8).bytes)), + SecretKey('backupKeychainSalt', () => hex.encode(encrypt.Key.fromSecureRandom(12).bytes)), SecretKey('changeNowApiKey', () => ''), SecretKey('wyreSecretKey', () => ''), SecretKey('wyreApiKey', () => ''), @@ -29,6 +24,8 @@ class SecretKey { SecretKey('anypayToken', () => ''), SecretKey('onramperApiKey', () => ''), SecretKey('ioniaClientId', () => ''), + SecretKey('trocadorApiKey', () => ''), + SecretKey('trocadorExchangeMarkup', () => ''), SecretKey('twitterBearerToken', () => ''), ];