diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index ddc8869f0..28af7cefb 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -148,6 +148,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 meldApiKey = '${{ secrets.MELD_API_KEY }}';" >> lib/.secrets.g.dart - name: Rename app run: | diff --git a/assets/images/meld_light.svg b/assets/images/meld_light.svg new file mode 100644 index 000000000..8fa80c378 --- /dev/null +++ b/assets/images/meld_light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/buy/meld/meld_provider.dart b/lib/buy/meld/meld_provider.dart new file mode 100644 index 000000000..fd118b432 --- /dev/null +++ b/lib/buy/meld/meld_provider.dart @@ -0,0 +1,160 @@ + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/buy/buy_provider.dart'; +import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class MeldProvider extends BuyProvider { + MeldProvider({ + required SettingsStore settingsStore, + required WalletBase wallet, + bool isTestEnvironment = false, + }) : baseSellUrl = isTestEnvironment ? _baseSellTestUrl : _baseSellProductUrl, + baseBuyUrl = isTestEnvironment ? _baseBuyTestUrl : _baseBuyProductUrl, + this._settingsStore = settingsStore, + super(wallet: wallet, isTestEnvironment: isTestEnvironment); + + final SettingsStore _settingsStore; + + static const _baseSellTestUrl = 'api-sb.meld.io'; + static const _baseSellProductUrl = 'api.meld.io'; + static const _baseBuyTestUrl = 'api-sb.meld.io'; + static const _baseBuyProductUrl = 'api.meld.io'; + // static const _cIdBaseUrl = 'exchange-helper.cakewallet.com'; + + final String baseBuyUrl; + final String baseSellUrl; + + @override + String get providerDescription => 'Meld provider description here'; + + @override + String get title => 'Meld'; + + @override + String get lightIcon => 'assets/images/meld_light.svg'; + + @override + String get darkIcon => 'assets/images/moonpay_dark.png'; + + static String themeToMoonPayTheme(ThemeBase theme) { + switch (theme.type) { + case ThemeType.bright: + case ThemeType.light: + return 'light'; + case ThemeType.dark: + return 'dark'; + } + } + + static String get _apiKey => secrets.moonPayApiKey; + + String get currencyCode => walletTypeToCryptoCurrency(wallet.type).title.toLowerCase(); + + + static String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey; + + Future requestSellUrl({ + required CryptoCurrency currency, + required String refundWalletAddress, + required SettingsStore settingsStore, + }) async { + throw UnimplementedError(); + } + + // BUY: + static const _currenciesSuffix = '/v3/currencies'; + static const _quoteSuffix = '/buy_quote'; + static const _transactionsSuffix = '/v1/transactions'; + static const _ipAddressSuffix = '/v4/ip_address'; + + Future requestBuyUrl({ + required CryptoCurrency currency, + required SettingsStore settingsStore, + required String walletAddress, + String? amount, + }) async { + final params = { + 'theme': themeToMoonPayTheme(settingsStore.currentTheme), + 'language': settingsStore.languageCode, + 'colorCode': settingsStore.currentTheme.type == ThemeType.dark + ? '#${Palette.blueCraiola.value.toRadixString(16).substring(2, 8)}' + : '#${Palette.moderateSlateBlue.value.toRadixString(16).substring(2, 8)}', + 'defaultCurrencyCode': _normalizeCurrency(currency), + 'baseCurrencyCode': _normalizeCurrency(currency), + 'baseCurrencyAmount': amount ?? '0', + 'currencyCode': currencyCode, + 'walletAddress': walletAddress, + 'lockAmount': 'true', + 'showAllCurrencies': 'false', + 'showWalletAddressForm': 'false', + 'enabledPaymentMethods': + 'credit_debit_card,apple_pay,google_pay,samsung_pay,sepa_bank_transfer,gbp_bank_transfer,gbp_open_banking_payment', + }; + + if (_apiKey.isNotEmpty) { + params['apiKey'] = _apiKey; + } + + final originalUri = Uri.https( + baseBuyUrl, + '', + params, + ); + + if (isTestEnvironment) { + return originalUri; + } + + return originalUri; + // final signature = await getMoonpaySignature('?${originalUri.query}'); + // final query = Map.from(originalUri.queryParameters); + // query['signature'] = signature; + // final signedUri = originalUri.replace(queryParameters: query); + // return signedUri; + } + + @override + Future launchProvider(BuildContext context, bool? isBuyAction) async { + late final Uri uri; + if (isBuyAction ?? true) { + uri = await requestBuyUrl( + currency: wallet.currency, + walletAddress: wallet.walletAddresses.address, + settingsStore: _settingsStore, + ); + } else { + uri = await requestSellUrl( + currency: wallet.currency, + refundWalletAddress: wallet.walletAddresses.address, + settingsStore: _settingsStore, + ); + } + + if (await canLaunchUrl(uri)) { + if (DeviceInfo.instance.isMobile) { + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['Meld', uri]); + } else { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } else { + throw Exception('Could not launch URL'); + } + } + + String _normalizeCurrency(CryptoCurrency currency) { + if (currency == CryptoCurrency.maticpoly) { + return "MATIC_POLYGON"; + } + + return currency.toString().toLowerCase(); + } +} diff --git a/lib/di.dart b/lib/di.dart index 291555330..e67b1a34c 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/anonpay/anonpay_api.dart'; import 'package:cake_wallet/anonpay/anonpay_info_base.dart'; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; +import 'package:cake_wallet/buy/meld/meld_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; @@ -813,6 +814,12 @@ Future setup({ isTestEnvironment: kDebugMode, )); + getIt.registerFactory(() => MeldProvider( + settingsStore: getIt.get().settingsStore, + wallet: getIt.get().wallet!, + isTestEnvironment: kDebugMode, + )); + getIt.registerFactory(() => OnRamperBuyProvider( getIt.get().settingsStore, wallet: getIt.get().wallet!, diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 701781cc2..3950ba5d1 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart'; +import 'package:cake_wallet/buy/meld/meld_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart'; @@ -12,6 +13,7 @@ enum ProviderType { dfx, onramper, moonpay, + meld, } extension ProviderTypeName on ProviderType { @@ -27,6 +29,8 @@ extension ProviderTypeName on ProviderType { return 'Onramper'; case ProviderType.moonpay: return 'MoonPay'; + case ProviderType.meld: + return 'Meld'; } } @@ -42,6 +46,8 @@ extension ProviderTypeName on ProviderType { return 'onramper_provider'; case ProviderType.moonpay: return 'moonpay_provider'; + case ProviderType.meld: + return 'meld_provider'; } } } @@ -63,10 +69,16 @@ class ProvidersHelper { ProviderType.dfx, ProviderType.robinhood, ProviderType.moonpay, + ProviderType.meld, ]; case WalletType.litecoin: case WalletType.bitcoinCash: - return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpay + ]; case WalletType.solana: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; case WalletType.none: @@ -115,6 +127,8 @@ class ProvidersHelper { return getIt.get(); case ProviderType.moonpay: return getIt.get(); + case ProviderType.meld: + return getIt.get(); case ProviderType.askEachTime: return null; }