mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
refactor: delete dead Cake Pay/buy/exchange code; split di setup()
- Cake Pay: 36 files (~5.3k LOC) plus DI registrations, routes, and UI branches (feature was hard-disabled and unreachable) - unreachable buy providers: robinhood, dfx, onramper, meld, kryptonim (~1.7k LOC); MoonPay untouched - exchange providers that only existed as commented-out wiring: changenow, sideshift, chainflip, exolix, swaptrade, letsexchange, stealthex, xoswap, swapsxyz, thorchain, simpleswap (~5.2k LOC); Trocador untouched; ExchangeProviderDescription values and Hive adapters kept so stored trades and orders still deserialize - di.dart setup() split from one 1,452-line function into 10 sequential chunk functions; registration execution order verified identical before and after (251 register calls)
This commit is contained in:
@@ -22,19 +22,6 @@ extension RecommendationTitle on ProviderRecommendation {
|
||||
}
|
||||
}
|
||||
|
||||
ProviderRecommendation? getRecommendationFromString(String title) {
|
||||
switch (title) {
|
||||
case 'BEST RATE':
|
||||
return ProviderRecommendation.bestRate;
|
||||
case 'LowKyc':
|
||||
return ProviderRecommendation.lowKyc;
|
||||
case 'SuccessRate':
|
||||
return ProviderRecommendation.successRate;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Quote extends SelectableOption {
|
||||
Quote({
|
||||
required this.rate,
|
||||
@@ -135,69 +122,6 @@ class Quote extends SelectableOption {
|
||||
|
||||
set setLimits(Limits limits) => this.limits = limits;
|
||||
|
||||
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
|
||||
Map<String, dynamic> metaData, PaymentType paymentType, String? customPaymentMethodType) {
|
||||
final rate = _toDouble(json['rate']) ?? 0.0;
|
||||
final networkFee = _toDouble(json['networkFee']) ?? 0.0;
|
||||
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
|
||||
final feeAmount = double.parse((networkFee + transactionFee).toStringAsFixed(2));
|
||||
|
||||
final rampId = json['ramp'] as String? ?? '';
|
||||
final rampData = metaData[rampId] ?? {};
|
||||
final rampName = rampData['displayName'] as String? ?? '';
|
||||
final rampIconPath = rampData['svg'] as String? ?? '';
|
||||
|
||||
final recommendations = json['recommendations'] != null
|
||||
? List<String>.from(json['recommendations'] as List<dynamic>)
|
||||
: <String>[];
|
||||
|
||||
final enumRecommendations = recommendations
|
||||
.map((e) => getRecommendationFromString(e))
|
||||
.whereType<ProviderRecommendation>()
|
||||
.toList();
|
||||
|
||||
final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
|
||||
double minLimit = 0.0;
|
||||
double maxLimit = double.infinity;
|
||||
|
||||
for (var paymentMethod in availablePaymentMethods) {
|
||||
if (paymentMethod is Map<String, dynamic>) {
|
||||
final details = paymentMethod['details'] as Map<String, dynamic>?;
|
||||
|
||||
if (details != null) {
|
||||
final limits = details['limits'] as Map<String, dynamic>?;
|
||||
|
||||
if (limits != null && limits.isNotEmpty) {
|
||||
final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
|
||||
if (firstLimitEntry != null) {
|
||||
minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
|
||||
maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Quote(
|
||||
rate: rate,
|
||||
feeAmount: feeAmount,
|
||||
networkFee: networkFee,
|
||||
transactionFee: transactionFee,
|
||||
payout: json['payout'] as double? ?? 0.0,
|
||||
rampId: rampId,
|
||||
rampName: rampName,
|
||||
rampIconPath: rampIconPath,
|
||||
paymentType: paymentType,
|
||||
customPaymentMethodType: customPaymentMethodType,
|
||||
quoteId: json['quoteId'] as String? ?? '',
|
||||
recommendations: enumRecommendations,
|
||||
provider: ProvidersHelper.getProviderByType(ProviderType.onramper),
|
||||
isBuyAction: isBuyAction,
|
||||
limits: Limits(min: minLimit, max: maxLimit),
|
||||
);
|
||||
}
|
||||
|
||||
factory Quote.fromMoonPayJson(
|
||||
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||
final rate = isBuyAction
|
||||
@@ -233,92 +157,6 @@ class Quote extends SelectableOption {
|
||||
);
|
||||
}
|
||||
|
||||
factory Quote.fromDFXJson(
|
||||
Map<String, dynamic> json,
|
||||
bool isBuyAction,
|
||||
PaymentType paymentType,
|
||||
) {
|
||||
final rate = _toDouble(json['exchangeRate']) ?? 0.0;
|
||||
final fees = json['fees'] as Map<String, dynamic>;
|
||||
|
||||
final minVolume = _toDouble(json['minVolume']) ?? 0.0;
|
||||
final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
|
||||
|
||||
return Quote(
|
||||
rate: isBuyAction ? rate : 1 / rate,
|
||||
feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
|
||||
networkFee: _toDouble(fees['network']) ?? 0.0,
|
||||
transactionFee: _toDouble(fees['rate']) ?? 0.0,
|
||||
payout: _toDouble(json['payout']) ?? 0.0,
|
||||
paymentType: paymentType,
|
||||
recommendations: [ProviderRecommendation.lowKyc],
|
||||
provider: ProvidersHelper.getProviderByType(ProviderType.dfx),
|
||||
isBuyAction: isBuyAction,
|
||||
limits: Limits(min: minVolume, max: maxVolume),
|
||||
);
|
||||
}
|
||||
|
||||
factory Quote.fromRobinhoodJson(
|
||||
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||
final networkFee = json['networkFee'] as Map<String, dynamic>;
|
||||
final processingFee = json['processingFee'] as Map<String, dynamic>;
|
||||
final networkFeeAmount = _toDouble(networkFee['fiatAmount']) ?? 0.0;
|
||||
final transactionFeeAmount = _toDouble(processingFee['fiatAmount']) ?? 0.0;
|
||||
final feeAmount = double.parse((networkFeeAmount + transactionFeeAmount).toStringAsFixed(2));
|
||||
|
||||
return Quote(
|
||||
rate: _toDouble(json['price']) ?? 0.0,
|
||||
feeAmount: feeAmount,
|
||||
networkFee: _toDouble(networkFee['fiatAmount']) ?? 0.0,
|
||||
transactionFee: _toDouble(processingFee['fiatAmount']) ?? 0.0,
|
||||
payout: _toDouble(json['cryptoAmount']) ?? 0.0,
|
||||
paymentType: paymentType,
|
||||
recommendations: [],
|
||||
provider: ProvidersHelper.getProviderByType(ProviderType.robinhood),
|
||||
isBuyAction: isBuyAction,
|
||||
limits: Limits(min: 0.0, max: double.infinity),
|
||||
);
|
||||
}
|
||||
|
||||
factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||
final quotes = json['quotes'][0] as Map<String, dynamic>;
|
||||
return Quote(
|
||||
rate: quotes['exchangeRate'] as double? ?? 0.0,
|
||||
feeAmount: quotes['totalFee'] as double? ?? 0.0,
|
||||
networkFee: quotes['networkFee'] as double? ?? 0.0,
|
||||
transactionFee: quotes['transactionFee'] as double? ?? 0.0,
|
||||
payout: quotes['payout'] as double? ?? 0.0,
|
||||
paymentType: paymentType,
|
||||
recommendations: [],
|
||||
provider: ProvidersHelper.getProviderByType(ProviderType.meld),
|
||||
isBuyAction: isBuyAction,
|
||||
limits: Limits(min: 0.0, max: double.infinity),
|
||||
);
|
||||
}
|
||||
|
||||
factory Quote.fromKryptonimJson(
|
||||
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
|
||||
final fees = json['fees'] as Map<String, dynamic>;
|
||||
// final rate = _toDouble(json['rate']) ?? 0.0;
|
||||
final limits = json['limits'] as Map<String, dynamic>;
|
||||
final minLimit = _toDouble(limits['min_amount']) ?? 0.0;
|
||||
final maxLimit = _toDouble(limits['max_amount']) ?? double.infinity;
|
||||
final convertedAmount = _toDouble(json['converted_amount']) ?? 0.0;
|
||||
final amount = _toDouble(json['amount']) ?? 0.0;
|
||||
final calculatedRate = amount / convertedAmount;
|
||||
return Quote(
|
||||
rate: calculatedRate,
|
||||
feeAmount: _toDouble(fees['totalFee']) ?? 0.0,
|
||||
networkFee: _toDouble(fees['network_fee']) ?? 0.0,
|
||||
transactionFee: _toDouble(fees['operation_fee']) ?? 0.0,
|
||||
payout: _toDouble(json['amount']) ?? 0.0,
|
||||
paymentType: paymentType,
|
||||
recommendations: [],
|
||||
provider: ProvidersHelper.getProviderByType(ProviderType.kriptonim),
|
||||
isBuyAction: isBuyAction,
|
||||
limits: Limits(min: minLimit, max: maxLimit));
|
||||
}
|
||||
|
||||
static double? _toDouble(dynamic value) {
|
||||
if (value is int) {
|
||||
return value.toDouble();
|
||||
|
||||
@@ -1,421 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/pairs_utils.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.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 DFXBuyProvider extends BuyProvider {
|
||||
DFXBuyProvider({
|
||||
required WalletBase wallet,
|
||||
bool isTestEnvironment = false,
|
||||
HardwareWalletViewModel? hardwareWalletVM,
|
||||
}) : super(
|
||||
wallet: wallet,
|
||||
isTestEnvironment: isTestEnvironment,
|
||||
hardwareWalletVM: hardwareWalletVM,
|
||||
supportedCryptoList: supportedCryptoToFiatPairs(
|
||||
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
|
||||
supportedFiatList: supportedFiatToCryptoPairs(
|
||||
notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto),
|
||||
);
|
||||
|
||||
static const _baseUrl = 'api.dfx.swiss';
|
||||
|
||||
// static const _signMessagePath = '/v1/auth/signMessage';
|
||||
static const _authPath = '/v1/auth';
|
||||
static const walletName = 'CakeWallet';
|
||||
|
||||
static final List<CryptoCurrency> _supportedCrypto = [
|
||||
CryptoCurrency.xmr,
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.maticpoly,
|
||||
];
|
||||
static final List<CryptoCurrency> _notSupportedCrypto = CryptoCurrency.all
|
||||
.where((crypto) => !_supportedCrypto.contains(crypto) || ["ETH", "POL"].contains(crypto.tag))
|
||||
.toList();
|
||||
|
||||
static final List<FiatCurrency> _supportedFiat = [FiatCurrency.chf, FiatCurrency.eur];
|
||||
static final List<FiatCurrency> _notSupportedFiat =
|
||||
FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList();
|
||||
|
||||
@override
|
||||
String get title => 'DFX.swiss';
|
||||
|
||||
@override
|
||||
String get providerDescription => S.current.dfx_option_description;
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/dfx_light.png';
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/dfx_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
String get blockchain {
|
||||
switch (wallet.type) {
|
||||
case WalletType.bitcoin:
|
||||
return 'Bitcoin';
|
||||
default:
|
||||
return walletTypeToString(wallet.type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<String> getSignMessage(String walletAddress) async =>
|
||||
"By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_Blockchain_address._Your_ID:_$walletAddress";
|
||||
|
||||
// Lets keep this just in case, but we can avoid this API Call
|
||||
// Future<String> getSignMessage() async {
|
||||
// final uri = Uri.https(_baseUrl, _signMessagePath, {'address': walletAddress});
|
||||
//
|
||||
// final response = await http.get(uri, headers: {'accept': 'application/json'});
|
||||
//
|
||||
// if (response.statusCode == 200) {
|
||||
// final responseBody = jsonDecode(response.body);
|
||||
// return responseBody['message'] as String;
|
||||
// } else {
|
||||
// throw Exception(
|
||||
// 'Failed to get sign message. Status: ${response.statusCode} ${response.body}');
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<String> auth(String walletAddress) async {
|
||||
final signMessage = await getSignature(await getSignMessage(walletAddress), walletAddress);
|
||||
|
||||
final requestBody = jsonEncode({
|
||||
'wallet': walletName,
|
||||
'address': walletAddress,
|
||||
'signature': signMessage,
|
||||
});
|
||||
|
||||
final uri = Uri.https(_baseUrl, _authPath);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final responseBody = jsonDecode(response.body);
|
||||
return responseBody['accessToken'] as String;
|
||||
} else if (response.statusCode == 403) {
|
||||
final responseBody = jsonDecode(response.body);
|
||||
final message = responseBody['message'] ?? 'Service unavailable in your country';
|
||||
throw Exception(message);
|
||||
} else {
|
||||
throw Exception('Failed to sign up. ${_getErrorMessage(response.statusCode, response.body)}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getSignature(String message, String walletAddress) async {
|
||||
switch (wallet.type) {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.arbitrum:
|
||||
case WalletType.bsc:
|
||||
return wallet.signMessage(message);
|
||||
case WalletType.monero:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return wallet.signMessage(message, address: walletAddress);
|
||||
default:
|
||||
throw Exception("WalletType is not available for DFX ${wallet.type}");
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchFiatCredentials(String fiatCurrency) async {
|
||||
final url = Uri.https(_baseUrl, '/v1/fiat');
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: url,
|
||||
headers: {'accept': 'application/json'});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as List<dynamic>;
|
||||
for (final item in data) {
|
||||
if (item['name'] == fiatCurrency) return item as Map<String, dynamic>;
|
||||
}
|
||||
log('DFX does not support fiat: $fiatCurrency');
|
||||
return {};
|
||||
} else {
|
||||
log('DFX Failed to fetch fiat currencies: ${response.statusCode}');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
printV('DFX Error fetching fiat currencies: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchAssetCredential(String assetsName) async {
|
||||
final url = Uri.https(_baseUrl, '/v1/asset', {'blockchains': blockchain});
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(clearnetUri: url, headers: {'accept': 'application/json'});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (responseData is List && responseData.isNotEmpty) {
|
||||
for (final i in responseData) {
|
||||
if (assetsName.toLowerCase() == i["dexName"].toString().toLowerCase()) {
|
||||
return i as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
return responseData.first as Map<String, dynamic>;
|
||||
} else if (responseData is Map<String, dynamic>) {
|
||||
return responseData;
|
||||
} else {
|
||||
log('DFX: Does not support this asset name : ${blockchain}');
|
||||
}
|
||||
} else {
|
||||
log('DFX: Failed to fetch assets: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
log('DFX: Error fetching assets: $e');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
|
||||
final List<PaymentMethod> paymentMethods = [];
|
||||
|
||||
if (isBuyAction) {
|
||||
final fiatBuyCredentials = await fetchFiatCredentials(fiatCurrency);
|
||||
if (fiatBuyCredentials.isNotEmpty) {
|
||||
fiatBuyCredentials.forEach((key, value) {
|
||||
if (key == 'limits') {
|
||||
final limits = value as Map<String, dynamic>;
|
||||
limits.forEach((paymentMethodKey, paymentMethodValue) {
|
||||
final min = _toDouble(paymentMethodValue['minVolume']);
|
||||
final max = _toDouble(paymentMethodValue['maxVolume']);
|
||||
if (min != null && max != null && min > 0 && max > 0) {
|
||||
final paymentMethod = PaymentMethod.fromDFX(
|
||||
paymentMethodKey, _getPaymentTypeByString(paymentMethodKey));
|
||||
paymentMethods.add(paymentMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
final assetCredentials = await fetchAssetCredential(cryptoCurrency.title);
|
||||
if (assetCredentials.isNotEmpty) {
|
||||
if (assetCredentials['sellable'] == true) {
|
||||
final availablePaymentTypes = [
|
||||
PaymentType.bankTransfer,
|
||||
PaymentType.creditCard,
|
||||
PaymentType.sepa
|
||||
];
|
||||
availablePaymentTypes.forEach((element) {
|
||||
final paymentMethod = PaymentMethod.fromDFX(normalizePaymentMethod(element)!, element);
|
||||
paymentMethods.add(paymentMethod);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Quote>?> fetchQuote(
|
||||
{required CryptoCurrency cryptoCurrency,
|
||||
required FiatCurrency fiatCurrency,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String walletAddress,
|
||||
PaymentType? paymentType,
|
||||
String? customPaymentMethodType,
|
||||
String? countryCode}) async {
|
||||
/// if buying with any currency other than eur or chf then DFX is not supported
|
||||
|
||||
if (isBuyAction && (fiatCurrency != FiatCurrency.eur && fiatCurrency != FiatCurrency.chf)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? paymentMethod;
|
||||
if (paymentType != null && paymentType != PaymentType.all) {
|
||||
paymentMethod = normalizePaymentMethod(paymentType);
|
||||
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||
} else {
|
||||
paymentMethod = 'Bank';
|
||||
}
|
||||
|
||||
final action = isBuyAction ? 'buy' : 'sell';
|
||||
|
||||
final fiatCredentials = await fetchFiatCredentials(fiatCurrency.name.toString());
|
||||
if (fiatCredentials['id'] == null) return null;
|
||||
|
||||
final assetCredentials = await fetchAssetCredential(cryptoCurrency.title.toString());
|
||||
if (assetCredentials['id'] == null) return null;
|
||||
|
||||
log('DFX: Fetching $action quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||
|
||||
final url = Uri.https(_baseUrl, '/v1/$action/quote');
|
||||
final headers = {'accept': 'application/json', 'Content-Type': 'application/json'};
|
||||
final body = jsonEncode({
|
||||
'currency': {'id': fiatCredentials['id'] as int},
|
||||
'asset': {'id': assetCredentials['id']},
|
||||
'amount': amount,
|
||||
'targetAmount': 0,
|
||||
'paymentMethod': paymentMethod,
|
||||
'discountCode': ''
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().put(
|
||||
clearnetUri: url,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
|
||||
final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
quote.setCryptoCurrency = cryptoCurrency;
|
||||
return [quote];
|
||||
} else {
|
||||
printV('DFX: Unexpected data type: ${responseData.runtimeType}');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if (responseData is Map<String, dynamic> && responseData.containsKey('message')) {
|
||||
printV('DFX Error: ${responseData['message']}');
|
||||
} else {
|
||||
printV('DFX Failed to fetch buy quote: ${response.statusCode}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
printV('DFX Error fetching buy quote: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
}));
|
||||
} else {
|
||||
hardwareWalletVM!.initWallet(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final actionType = isBuyAction ? '/buy' : '/sell';
|
||||
|
||||
final accessToken = await auth(cryptoCurrencyAddress);
|
||||
|
||||
final uri = Uri.https('app.dfx.swiss', actionType, {
|
||||
'session': accessToken,
|
||||
'lang': 'en',
|
||||
'asset-out': isBuyAction ? quote.cryptoCurrency.toString() : quote.fiatCurrency.toString(),
|
||||
'blockchain': blockchain,
|
||||
'asset-in': isBuyAction ? quote.fiatCurrency.toString() : quote.cryptoCurrency.toString(),
|
||||
'amount-in': amount.toString()
|
||||
});
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw Exception('Could not launch URL');
|
||||
}
|
||||
} catch (e) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "DFX.swiss",
|
||||
alertContent: '${S.of(context).buy_provider_unavailable}: $e',
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? normalizePaymentMethod(PaymentType paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case PaymentType.bankTransfer:
|
||||
return 'Bank';
|
||||
case PaymentType.creditCard:
|
||||
return 'Card';
|
||||
case PaymentType.sepa:
|
||||
return 'Instant';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case 'Bank':
|
||||
return PaymentType.bankTransfer;
|
||||
case 'Card':
|
||||
return PaymentType.creditCard;
|
||||
case 'Instant':
|
||||
return PaymentType.sepa;
|
||||
default:
|
||||
return PaymentType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
double? _toDouble(dynamic value) {
|
||||
if (value is int) {
|
||||
return value.toDouble();
|
||||
} else if (value is double) {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getErrorMessage(int statusCode, String body) {
|
||||
final responseBody = jsonDecode(body) as Map<String, dynamic>;
|
||||
final message = responseBody['message']?.toString() ?? '';
|
||||
|
||||
if (message.contains("address must match") || message.contains("signature must match")) {
|
||||
return "The wallet type must match the selected currency";
|
||||
}
|
||||
|
||||
return message.isNotEmpty ? message : "Unknown error: ${statusCode}";
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/pairs_utils.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:developer';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class KryptonimBuyProvider extends BuyProvider {
|
||||
KryptonimBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
|
||||
: super(
|
||||
wallet: wallet,
|
||||
isTestEnvironment: isTestEnvironment,
|
||||
hardwareWalletVM: null,
|
||||
supportedCryptoList: supportedCryptoToFiatPairs(
|
||||
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
|
||||
supportedFiatList: supportedFiatToCryptoPairs(
|
||||
notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto));
|
||||
|
||||
static const _isProduction = true;
|
||||
|
||||
static const _baseUrl = _isProduction ? 'app.kryptonim.com' : 'intg-api.kryptonim.com';
|
||||
static const _baseWidgetUrl = _isProduction ? 'buy.kryptonim.com' : 'intg.kryptonim.com';
|
||||
static const _quotePath = '/v2/ramp/buy/quotes';
|
||||
static const _merchantId = 'a70fe053';
|
||||
|
||||
static String get _kryptonimApiKey => secrets.kryptonimApiKey;
|
||||
|
||||
static const List<CryptoCurrency> _notSupportedCrypto = [];
|
||||
static const List<FiatCurrency> _notSupportedFiat = [];
|
||||
|
||||
@override
|
||||
String get title => 'Kryptonim';
|
||||
|
||||
@override
|
||||
String get providerDescription => 'Kryptonim Buy Provider';
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/kryptonim_light.png';
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/kryptonim_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
Future<Map<String, dynamic>> getExchangeRates(
|
||||
{required CryptoCurrency cryptoCurrency,
|
||||
required String fiatCurrency,
|
||||
required double amount}) async {
|
||||
final url = Uri.https(_baseUrl, _quotePath, {'m': _merchantId});
|
||||
|
||||
final headers = {
|
||||
'accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': _kryptonimApiKey,
|
||||
};
|
||||
|
||||
try {
|
||||
final body = jsonEncode({
|
||||
'amount': amount,
|
||||
'currency': fiatCurrency,
|
||||
'converted_currency': cryptoCurrency.title,
|
||||
'blockchain': _normalizeBlockChain(cryptoCurrency),
|
||||
'quote_currency': fiatCurrency,
|
||||
});
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: url,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201 || response.statusCode == 401) {
|
||||
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
|
||||
|
||||
final data = await getExchangeRates(
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
fiatCurrency: fiatCurrency,
|
||||
amount: 100.0,
|
||||
);
|
||||
|
||||
if (data.isEmpty || !data.containsKey('data')) return [];
|
||||
|
||||
final paymentMethods = (data['data'] as List<dynamic>)
|
||||
.map((e) => PaymentMethod.fromKryptonimJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Quote>?> fetchQuote({
|
||||
required CryptoCurrency cryptoCurrency,
|
||||
required FiatCurrency fiatCurrency,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String walletAddress,
|
||||
PaymentType? paymentType,
|
||||
String? customPaymentMethodType,
|
||||
String? countryCode,
|
||||
}) async {
|
||||
log('Kryptonim: Fetching quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
|
||||
|
||||
final data = await getExchangeRates(
|
||||
cryptoCurrency: cryptoCurrency,
|
||||
fiatCurrency: fiatCurrency.toString(),
|
||||
amount: amount,
|
||||
);
|
||||
|
||||
if (!data.containsKey('data') || (data['data'] as List).isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final quotesList = data['data'] as List<dynamic>;
|
||||
|
||||
Map<String, dynamic>? selectedPaymentMethod;
|
||||
|
||||
if (paymentType == PaymentType.all || paymentType == null) {
|
||||
selectedPaymentMethod = quotesList.first as Map<String, dynamic>;
|
||||
} else {
|
||||
for (var quote in quotesList) {
|
||||
final quotePaymentType = PaymentMethod.getPaymentTypeId(quote['payment_method'] as String?);
|
||||
if (quotePaymentType == paymentType) {
|
||||
selectedPaymentMethod = quote as Map<String, dynamic>;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final selectedPaymentType =
|
||||
PaymentMethod.getPaymentTypeId(selectedPaymentMethod['payment_method'] as String?);
|
||||
final quote = Quote.fromKryptonimJson(selectedPaymentMethod, isBuyAction, selectedPaymentType ?? PaymentType.unknown);
|
||||
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
quote.setCryptoCurrency = cryptoCurrency;
|
||||
|
||||
return [quote];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
final params = {
|
||||
'amount': amount.toInt().toString(),
|
||||
'currency': quote.fiatCurrency.name,
|
||||
'convertedCurrency': quote.cryptoCurrency.title,
|
||||
'blockchain': _normalizeBlockChain(quote.cryptoCurrency),
|
||||
'address': cryptoCurrencyAddress,
|
||||
'paymentMethod': normalizePaymentMethod(quote.paymentType),
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseWidgetUrl, '/redirect-form', params);
|
||||
|
||||
try {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw Exception('Could not launch URL');
|
||||
}
|
||||
} catch (e) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: "Kryptonim",
|
||||
alertContent: "Payment provider is unavailable: $e",
|
||||
buttonText: "OK",
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String normalizePaymentMethod(PaymentType paymentType) {
|
||||
switch (paymentType) {
|
||||
case PaymentType.bankTransfer:
|
||||
return 'bank';
|
||||
case PaymentType.creditCard:
|
||||
case PaymentType.debitCard:
|
||||
return 'card';
|
||||
default:
|
||||
return paymentType.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeBlockChain(CryptoCurrency cur) {
|
||||
String? blockchain = switch (cur.tag) {
|
||||
'ETH' => 'Ethereum',
|
||||
'BASE' => 'Base',
|
||||
'ARB' => 'Arbitrum',
|
||||
'POL' => 'Polygon',
|
||||
'AVAXC' => 'Avalanche',
|
||||
'SOL' => 'Solana',
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (blockchain == null) {
|
||||
blockchain = switch (cur) {
|
||||
CryptoCurrency.btc => 'Bitcoin',
|
||||
CryptoCurrency.ltc => 'Litecoin',
|
||||
CryptoCurrency.eth => 'Ethereum',
|
||||
CryptoCurrency.baseEth => 'Base',
|
||||
CryptoCurrency.arbEth => 'Arbitrum',
|
||||
CryptoCurrency.maticpoly => 'Matic',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
return blockchain ?? cur.fullName ?? '';
|
||||
}
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/pairs_utils.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:developer';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MeldBuyProvider extends BuyProvider {
|
||||
MeldBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
|
||||
: super(
|
||||
wallet: wallet,
|
||||
isTestEnvironment: isTestEnvironment,
|
||||
hardwareWalletVM: null,
|
||||
supportedCryptoList: supportedCryptoToFiatPairs(
|
||||
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
|
||||
supportedFiatList: supportedFiatToCryptoPairs(
|
||||
notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto));
|
||||
|
||||
static const _isProduction = false;
|
||||
|
||||
static const _baseUrl = _isProduction ? 'api.meld.io' : 'api-sb.meld.io';
|
||||
static const _providersProperties = '/service-providers/properties';
|
||||
static const _paymentMethodsPath = '/payment-methods';
|
||||
static const _quotePath = '/payments/crypto/quote';
|
||||
|
||||
static const String sandboxUrl = 'sb.fluidmoney.xyz';
|
||||
static const String productionUrl = 'fluidmoney.xyz';
|
||||
|
||||
static const String _baseWidgetUrl = _isProduction ? productionUrl : sandboxUrl;
|
||||
|
||||
static String get _testApiKey => secrets.meldTestApiKey;
|
||||
|
||||
static const List<CryptoCurrency> _notSupportedCrypto = [];
|
||||
static const List<FiatCurrency> _notSupportedFiat = [];
|
||||
|
||||
static String get _testPublicKey => '' ; //secrets.meldTestPublicKey;
|
||||
|
||||
@override
|
||||
String get title => 'Meld';
|
||||
|
||||
@override
|
||||
String get providerDescription => 'Meld Buy Provider';
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/meld_logo.svg';
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/meld_logo.svg';
|
||||
|
||||
@override
|
||||
bool get isAggregator => true;
|
||||
|
||||
@override
|
||||
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
|
||||
final params = {'fiatCurrencies': fiatCurrency, 'statuses': 'LIVE,RECENTLY_ADDED,BUILDING'};
|
||||
|
||||
final path = '$_providersProperties$_paymentMethodsPath';
|
||||
final url = Uri.https(_baseUrl, path, params);
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: url,
|
||||
headers: {
|
||||
'Authorization': _isProduction ? '' : _testApiKey,
|
||||
'Meld-Version': '2023-12-19',
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as List<dynamic>;
|
||||
final paymentMethods =
|
||||
data.map((e) => PaymentMethod.fromMeldJson(e as Map<String, dynamic>)).toList();
|
||||
return paymentMethods;
|
||||
} else {
|
||||
printV('Meld: Failed to fetch payment types');
|
||||
return List<PaymentMethod>.empty();
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Meld: Failed to fetch payment types: $e');
|
||||
return List<PaymentMethod>.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Quote>?> fetchQuote(
|
||||
{required CryptoCurrency cryptoCurrency,
|
||||
required FiatCurrency fiatCurrency,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String walletAddress,
|
||||
PaymentType? paymentType,
|
||||
String? customPaymentMethodType,
|
||||
String? countryCode}) async {
|
||||
String? paymentMethod;
|
||||
if (paymentType != null && paymentType != PaymentType.all) {
|
||||
paymentMethod = normalizePaymentMethod(paymentType);
|
||||
if (paymentMethod == null) paymentMethod = paymentType.name;
|
||||
}
|
||||
|
||||
log('Meld: Fetching buy quote: ${isBuyAction ? cryptoCurrency : fiatCurrency} -> ${isBuyAction ? fiatCurrency : cryptoCurrency}, amount: $amount');
|
||||
|
||||
final url = Uri.https(_baseUrl, _quotePath);
|
||||
final headers = {
|
||||
'Authorization': _testApiKey,
|
||||
'Meld-Version': '2023-12-19',
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
final body = jsonEncode({
|
||||
'countryCode': countryCode,
|
||||
'destinationCurrencyCode': isBuyAction ? fiatCurrency.name : cryptoCurrency.title,
|
||||
'sourceAmount': amount,
|
||||
'sourceCurrencyCode': isBuyAction ? cryptoCurrency.title : fiatCurrency.name,
|
||||
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: url,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final paymentType = _getPaymentTypeByString(data['paymentMethodType'] as String?);
|
||||
final quote = Quote.fromMeldJson(data, isBuyAction, paymentType);
|
||||
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
quote.setCryptoCurrency = cryptoCurrency;
|
||||
|
||||
return [quote];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error fetching buy quote: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
final actionType = isBuyAction ? 'BUY' : 'SELL';
|
||||
|
||||
final params = {
|
||||
'publicKey': _isProduction ? '' : _testPublicKey,
|
||||
'countryCode': countryCode,
|
||||
//'paymentMethodType': normalizePaymentMethod(paymentMethod.paymentMethodType),
|
||||
'sourceAmount': amount.toString(),
|
||||
'sourceCurrencyCode': quote.fiatCurrency,
|
||||
'destinationCurrencyCode': quote.cryptoCurrency,
|
||||
'walletAddress': cryptoCurrencyAddress,
|
||||
'transactionType': actionType
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseWidgetUrl, '', params);
|
||||
|
||||
try {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw Exception('Could not launch URL');
|
||||
}
|
||||
} catch (e) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: "Meld",
|
||||
alertContent: S.of(context).buy_provider_unavailable + ': $e',
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? normalizePaymentMethod(PaymentType paymentType) {
|
||||
switch (paymentType) {
|
||||
case PaymentType.creditCard:
|
||||
return 'CREDIT_DEBIT_CARD';
|
||||
case PaymentType.applePay:
|
||||
return 'APPLE_PAY';
|
||||
case PaymentType.googlePay:
|
||||
return 'GOOGLE_PAY';
|
||||
case PaymentType.neteller:
|
||||
return 'NETELLER';
|
||||
case PaymentType.skrill:
|
||||
return 'SKRILL';
|
||||
case PaymentType.sepa:
|
||||
return 'SEPA';
|
||||
case PaymentType.sepaInstant:
|
||||
return 'SEPA_INSTANT';
|
||||
case PaymentType.ach:
|
||||
return 'ACH';
|
||||
case PaymentType.achInstant:
|
||||
return 'INSTANT_ACH';
|
||||
case PaymentType.Khipu:
|
||||
return 'KHIPU';
|
||||
case PaymentType.ovo:
|
||||
return 'OVO';
|
||||
case PaymentType.zaloPay:
|
||||
return 'ZALOPAY';
|
||||
case PaymentType.zaloBankTransfer:
|
||||
return 'ZA_BANK_TRANSFER';
|
||||
case PaymentType.gcash:
|
||||
return 'GCASH';
|
||||
case PaymentType.imps:
|
||||
return 'IMPS';
|
||||
case PaymentType.dana:
|
||||
return 'DANA';
|
||||
case PaymentType.ideal:
|
||||
return 'IDEAL';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||
switch (paymentMethod?.toUpperCase()) {
|
||||
case 'CREDIT_DEBIT_CARD':
|
||||
return PaymentType.creditCard;
|
||||
case 'APPLE_PAY':
|
||||
return PaymentType.applePay;
|
||||
case 'GOOGLE_PAY':
|
||||
return PaymentType.googlePay;
|
||||
case 'NETELLER':
|
||||
return PaymentType.neteller;
|
||||
case 'SKRILL':
|
||||
return PaymentType.skrill;
|
||||
case 'SEPA':
|
||||
return PaymentType.sepa;
|
||||
case 'SEPA_INSTANT':
|
||||
return PaymentType.sepaInstant;
|
||||
case 'ACH':
|
||||
return PaymentType.ach;
|
||||
case 'INSTANT_ACH':
|
||||
return PaymentType.achInstant;
|
||||
case 'KHIPU':
|
||||
return PaymentType.Khipu;
|
||||
case 'OVO':
|
||||
return PaymentType.ovo;
|
||||
case 'ZALOPAY':
|
||||
return PaymentType.zaloPay;
|
||||
case 'ZA_BANK_TRANSFER':
|
||||
return PaymentType.zaloBankTransfer;
|
||||
case 'GCASH':
|
||||
return PaymentType.gcash;
|
||||
case 'IMPS':
|
||||
return PaymentType.imps;
|
||||
case 'DANA':
|
||||
return PaymentType.dana;
|
||||
case 'IDEAL':
|
||||
return PaymentType.ideal;
|
||||
default:
|
||||
return PaymentType.all;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,453 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/pairs_utils.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class OnRamperBuyProvider extends BuyProvider {
|
||||
OnRamperBuyProvider(
|
||||
{required WalletBase wallet, bool isTestEnvironment = false})
|
||||
: super(wallet: wallet,
|
||||
isTestEnvironment: isTestEnvironment,
|
||||
hardwareWalletVM: null,
|
||||
supportedCryptoList: supportedCryptoToFiatPairs(
|
||||
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
|
||||
supportedFiatList: supportedFiatToCryptoPairs(
|
||||
notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto));
|
||||
|
||||
static const _baseUrl = 'buy.onramper.com';
|
||||
static const _baseApiUrl = 'api.onramper.com';
|
||||
static const _cIdBaseUrl = 'exchange-helper.such.software';
|
||||
static const quotes = '/quotes';
|
||||
static const paymentTypes = '/payment-types';
|
||||
static const supported = '/supported';
|
||||
static const defaultsAll = '/defaults/all';
|
||||
|
||||
static const List<CryptoCurrency> _notSupportedCrypto = [];
|
||||
static const List<FiatCurrency> _notSupportedFiat = [];
|
||||
static Map<String, dynamic> _onrampMetadata = {};
|
||||
|
||||
String? recommendedPaymentType;
|
||||
|
||||
String get _apiKey => secrets.onramperApiKey;
|
||||
|
||||
String get _exchangeHelperApiKey => secrets.exchangeHelperApiKey;
|
||||
|
||||
@override
|
||||
String get title => 'Onramper';
|
||||
|
||||
@override
|
||||
String get providerDescription => S.current.onramper_option_description;
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/onramper_light.png';
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/onramper_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => true;
|
||||
|
||||
Future<String> getOnramperSignature(String query) async {
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/onramper");
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: {'Content-Type': 'application/json', 'x-api-key': _exchangeHelperApiKey},
|
||||
body: json.encode({'query': query}),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return (jsonDecode(response.body) as Map<String, dynamic>)['signature'] as String;
|
||||
} else {
|
||||
throw Exception(
|
||||
'Provider currently unavailable. Status: ${response.statusCode} ${response.body}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getRecommendedPaymentType(bool isBuyAction) async {
|
||||
|
||||
final params = {'type': isBuyAction ? 'buy' : 'sell'};
|
||||
|
||||
final url = Uri.https(_baseApiUrl, '$supported$defaultsAll', params);
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: url,
|
||||
headers: {'Authorization': _apiKey, 'accept': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final recommended = data['message']['recommended'] as Map<String, dynamic>;
|
||||
|
||||
final recommendedPaymentType = recommended['paymentMethod'] as String?;
|
||||
|
||||
return recommendedPaymentType ;
|
||||
} else {
|
||||
final responseBody =
|
||||
jsonDecode(response.body) as Map<String, dynamic>;
|
||||
printV('Failed to fetch available payment types: ${responseBody['message']}');
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Failed to fetch available payment types: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<PaymentMethod>> getAvailablePaymentTypes(
|
||||
String fiatCurrency, CryptoCurrency cryptoCurrency, bool isBuyAction) async {
|
||||
|
||||
final normalizedCryptoCurrency =
|
||||
cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency);
|
||||
|
||||
final sourceCurrency = (isBuyAction ? fiatCurrency : normalizedCryptoCurrency).toLowerCase();
|
||||
final destinationCurrency = (isBuyAction ? normalizedCryptoCurrency : fiatCurrency).toLowerCase();
|
||||
|
||||
final params = {'type': isBuyAction ? 'buy' : 'sell', 'destination' : destinationCurrency};
|
||||
|
||||
final url = Uri.https(_baseApiUrl, '$supported$paymentTypes/$sourceCurrency', params);
|
||||
|
||||
try {
|
||||
final response =
|
||||
await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final List<dynamic> message = data['message'] as List<dynamic>;
|
||||
|
||||
final allAvailablePaymentMethods = message
|
||||
.map((item) => PaymentMethod.fromOnramperJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
recommendedPaymentType = await getRecommendedPaymentType(isBuyAction);
|
||||
|
||||
return allAvailablePaymentMethods;
|
||||
} else {
|
||||
final responseBody =
|
||||
jsonDecode(response.body) as Map<String, dynamic>;
|
||||
printV('Failed to fetch available payment types: ${responseBody['message']}');
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Failed to fetch available payment types: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getOnrampMetadata() async {
|
||||
final url = Uri.https(_baseApiUrl, '$supported/onramps/all');
|
||||
|
||||
try {
|
||||
final response =
|
||||
await ProxyWrapper().get(clearnetUri: url, headers: {'Authorization': _apiKey, 'accept': 'application/json'});
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final List<dynamic> onramps = data['message'] as List<dynamic>;
|
||||
|
||||
final Map<String, dynamic> result = {
|
||||
for (var onramp in onramps)
|
||||
(onramp['id'] as String): {
|
||||
'displayName': onramp['displayName'] as String,
|
||||
'svg': onramp['icons']['svg'] as String
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
} else {
|
||||
printV('Failed to fetch onramp metadata');
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Error occurred: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Quote>?> fetchQuote(
|
||||
{required CryptoCurrency cryptoCurrency,
|
||||
required FiatCurrency fiatCurrency,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String walletAddress,
|
||||
PaymentType? paymentType,
|
||||
String? customPaymentMethodType,
|
||||
String? countryCode}) async {
|
||||
String? paymentMethod;
|
||||
|
||||
if (paymentType == PaymentType.all ) paymentMethod = null;
|
||||
else if (paymentType == PaymentType.unknown) paymentMethod = customPaymentMethodType;
|
||||
else if (paymentType != null) paymentMethod = normalizePaymentMethod(paymentType);
|
||||
|
||||
final actionType = isBuyAction ? 'buy' : 'sell';
|
||||
|
||||
final normalizedCryptoCurrency =
|
||||
cryptoCurrency.title + _getNormalizeNetwork(cryptoCurrency).toUpperCase();
|
||||
|
||||
final params = {
|
||||
'amount': amount.toString(),
|
||||
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||
'clientName': 'CakeWallet',
|
||||
if (actionType == 'sell') 'type': actionType,
|
||||
};
|
||||
|
||||
log('Onramper: Fetching $actionType quote: ${isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name} -> ${isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency}, amount: $amount, paymentMethod: $paymentMethod');
|
||||
|
||||
final sourceCurrency = isBuyAction ? fiatCurrency.name : normalizedCryptoCurrency;
|
||||
final destinationCurrency = isBuyAction ? normalizedCryptoCurrency : fiatCurrency.name;
|
||||
|
||||
final url = Uri.https(_baseApiUrl, '$quotes/${sourceCurrency}/${destinationCurrency}', params);
|
||||
final headers = {'Authorization': _apiKey, 'accept': 'application/json'};
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(clearnetUri: url, headers: headers);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as List<dynamic>;
|
||||
if (data.isEmpty) return null;
|
||||
|
||||
List<Quote> validQuotes = [];
|
||||
|
||||
if (_onrampMetadata.isEmpty) _onrampMetadata = await getOnrampMetadata();
|
||||
|
||||
for (var item in data) {
|
||||
|
||||
if (item['errors'] != null) continue;
|
||||
|
||||
final paymentMethod = (item as Map<String, dynamic>)['paymentMethod'] as String;
|
||||
|
||||
final rampId = item['ramp'] as String?;
|
||||
final rampMetaData = _onrampMetadata[rampId] as Map<String, dynamic>?;
|
||||
|
||||
if (rampMetaData == null) continue;
|
||||
|
||||
final quote = Quote.fromOnramperJson(
|
||||
item, isBuyAction, _onrampMetadata, _getPaymentTypeByString(paymentMethod), customPaymentMethodType);
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
quote.setCryptoCurrency = cryptoCurrency;
|
||||
validQuotes.add(quote);
|
||||
}
|
||||
|
||||
if (validQuotes.isEmpty) return null;
|
||||
|
||||
return validQuotes;
|
||||
} else {
|
||||
printV('Onramper: Failed to fetch rate');
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Onramper: Failed to fetch rate $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
final actionType = isBuyAction ? 'buy' : 'sell';
|
||||
|
||||
final primaryColor = getColorStr(Theme.of(context).colorScheme.primary,);
|
||||
final secondaryColor = getColorStr(Theme.of(context).colorScheme.surface);
|
||||
final primaryTextColor = getColorStr(Theme.of(context).colorScheme.onSurface);
|
||||
final secondaryTextColor =
|
||||
getColorStr(Theme.of(context).colorScheme.onSurfaceVariant);
|
||||
final containerColor = getColorStr(Theme.of(context).colorScheme.surface);
|
||||
var cardColor = getColorStr(Theme.of(context).colorScheme.surfaceContainer);
|
||||
|
||||
final defaultCrypto =
|
||||
quote.cryptoCurrency.title + _getNormalizeNetwork(quote.cryptoCurrency).toLowerCase();
|
||||
|
||||
final paymentMethod = quote.paymentType == PaymentType.unknown ? quote.customPaymentMethodType : normalizePaymentMethod(quote.paymentType);
|
||||
|
||||
final networkWallets =
|
||||
'${_tagToNetwork(quote.cryptoCurrency.tag ?? quote.cryptoCurrency.title).toLowerCase()}:$cryptoCurrencyAddress';
|
||||
|
||||
final signature = await getOnramperSignature("networkWallets=$networkWallets");
|
||||
|
||||
final uri = Uri.https(_baseUrl, '', {
|
||||
'apiKey': _apiKey,
|
||||
'txnType': actionType,
|
||||
'txnFiat': quote.fiatCurrency.name,
|
||||
'txnCrypto': defaultCrypto,
|
||||
'txnAmount': amount.toString(),
|
||||
'skipTransactionScreen': "true",
|
||||
if (paymentMethod != null) 'txnPaymentMethod': paymentMethod,
|
||||
'txnOnramp': quote.rampId,
|
||||
'networkWallets': networkWallets,
|
||||
'supportSwap': "false",
|
||||
'primaryColor': primaryColor,
|
||||
'secondaryColor': secondaryColor,
|
||||
'containerColor': containerColor,
|
||||
'primaryTextColor': primaryTextColor,
|
||||
'secondaryTextColor': secondaryTextColor,
|
||||
'cardColor': cardColor,
|
||||
'signature': signature,
|
||||
});
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw Exception('Could not launch URL ${uri.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
List<CryptoCurrency> mainCurrency = [
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.sol,
|
||||
];
|
||||
|
||||
String _tagToNetwork(String tag) {
|
||||
switch (tag) {
|
||||
case 'POL':
|
||||
return 'POLYGON';
|
||||
case 'ARB':
|
||||
return 'ARBITRUM';
|
||||
case 'ETH':
|
||||
return 'ETHEREUM';
|
||||
case 'TRX':
|
||||
return 'TRON';
|
||||
case 'SOL':
|
||||
return 'SOLANA';
|
||||
case 'ZEC':
|
||||
return 'ZCASH';
|
||||
default:
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
String _getNormalizeNetwork(CryptoCurrency currency) {
|
||||
if (mainCurrency.contains(currency)) return '';
|
||||
|
||||
if (currency == CryptoCurrency.eos) return '_EOSIO';
|
||||
|
||||
if (currency.tag != null) return '_' + _tagToNetwork(currency.tag!);
|
||||
|
||||
return '_' + (currency.fullName?.replaceAll(' ', '') ?? currency.title);
|
||||
}
|
||||
|
||||
String? normalizePaymentMethod(PaymentType paymentType) {
|
||||
switch (paymentType) {
|
||||
case PaymentType.bankTransfer:
|
||||
return 'banktransfer';
|
||||
case PaymentType.creditCard:
|
||||
return 'creditcard';
|
||||
case PaymentType.debitCard:
|
||||
return 'debitcard';
|
||||
case PaymentType.applePay:
|
||||
return 'applepay';
|
||||
case PaymentType.googlePay:
|
||||
return 'googlepay';
|
||||
case PaymentType.revolutPay:
|
||||
return 'revolutpay';
|
||||
case PaymentType.neteller:
|
||||
return 'neteller';
|
||||
case PaymentType.skrill:
|
||||
return 'skrill';
|
||||
case PaymentType.sepa:
|
||||
return 'sepabanktransfer';
|
||||
case PaymentType.sepaInstant:
|
||||
return 'sepainstant';
|
||||
case PaymentType.ach:
|
||||
return 'ach';
|
||||
case PaymentType.achInstant:
|
||||
return 'iach';
|
||||
case PaymentType.Khipu:
|
||||
return 'khipu';
|
||||
case PaymentType.palomaBanktTansfer:
|
||||
return 'palomabanktransfer';
|
||||
case PaymentType.ovo:
|
||||
return 'ovo';
|
||||
case PaymentType.zaloPay:
|
||||
return 'zalopay';
|
||||
case PaymentType.zaloBankTransfer:
|
||||
return 'zalobanktransfer';
|
||||
case PaymentType.gcash:
|
||||
return 'gcash';
|
||||
case PaymentType.imps:
|
||||
return 'imps';
|
||||
case PaymentType.dana:
|
||||
return 'dana';
|
||||
case PaymentType.ideal:
|
||||
return 'ideal';
|
||||
case PaymentType.pixPay:
|
||||
return 'pix';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType _getPaymentTypeByString(String paymentMethod) {
|
||||
switch (paymentMethod.toLowerCase()) {
|
||||
case 'banktransfer':
|
||||
return PaymentType.bankTransfer;
|
||||
case 'creditcard':
|
||||
return PaymentType.creditCard;
|
||||
case 'debitcard':
|
||||
return PaymentType.debitCard;
|
||||
case 'applepay':
|
||||
return PaymentType.applePay;
|
||||
case 'googlepay':
|
||||
return PaymentType.googlePay;
|
||||
case 'revolutpay':
|
||||
return PaymentType.revolutPay;
|
||||
case 'neteller':
|
||||
return PaymentType.neteller;
|
||||
case 'skrill':
|
||||
return PaymentType.skrill;
|
||||
case 'sepabanktransfer':
|
||||
return PaymentType.sepa;
|
||||
case 'sepainstant':
|
||||
return PaymentType.sepaInstant;
|
||||
case 'ach':
|
||||
return PaymentType.ach;
|
||||
case 'iach':
|
||||
return PaymentType.achInstant;
|
||||
case 'khipu':
|
||||
return PaymentType.Khipu;
|
||||
case 'palomabanktransfer':
|
||||
return PaymentType.palomaBanktTansfer;
|
||||
case 'ovo':
|
||||
return PaymentType.ovo;
|
||||
case 'zalopay':
|
||||
return PaymentType.zaloPay;
|
||||
case 'zalobanktransfer':
|
||||
return PaymentType.zaloBankTransfer;
|
||||
case 'gcash':
|
||||
return PaymentType.gcash;
|
||||
case 'imps':
|
||||
return PaymentType.imps;
|
||||
case 'dana':
|
||||
return PaymentType.dana;
|
||||
case 'ideal':
|
||||
return PaymentType.ideal;
|
||||
case 'pix':
|
||||
return PaymentType.pixPay;
|
||||
default:
|
||||
return PaymentType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
String getColorStr(Color color) =>
|
||||
color.toARGB32().toRadixString(16).replaceAll(RegExp(r'^ff'), "");
|
||||
}
|
||||
@@ -193,23 +193,6 @@ class PaymentMethod extends SelectableOption {
|
||||
customIconPath: 'assets/images/dollar_coin.svg');
|
||||
}
|
||||
|
||||
factory PaymentMethod.fromOnramperJson(Map<String, dynamic> json) {
|
||||
final type = PaymentMethod.getPaymentTypeId(json['paymentTypeId'] as String?);
|
||||
return PaymentMethod(
|
||||
paymentMethodType: type ?? PaymentType.unknown,
|
||||
customPaymentMethodType: json['paymentTypeId'] as String?,
|
||||
customTitle: json['name'] as String? ?? 'Unknown',
|
||||
customIconPath: json['icon'] as String? ?? 'assets/images/card.png',
|
||||
customDescription: json['description'] as String?);
|
||||
}
|
||||
|
||||
factory PaymentMethod.fromDFX(String paymentMethod, PaymentType paymentType) {
|
||||
return PaymentMethod(
|
||||
paymentMethodType: paymentType,
|
||||
customTitle: paymentMethod,
|
||||
customIconPath: 'assets/images/card.png');
|
||||
}
|
||||
|
||||
factory PaymentMethod.fromMoonPayJson(Map<String, dynamic> json, PaymentType paymentType) {
|
||||
return PaymentMethod(
|
||||
paymentMethodType: paymentType,
|
||||
@@ -217,25 +200,6 @@ class PaymentMethod extends SelectableOption {
|
||||
customIconPath: 'assets/images/card.png');
|
||||
}
|
||||
|
||||
factory PaymentMethod.fromMeldJson(Map<String, dynamic> json) {
|
||||
final type = PaymentMethod.getPaymentTypeId(json['paymentMethod'] as String?);
|
||||
final logos = json['logos'] as Map<String, dynamic>;
|
||||
return PaymentMethod(
|
||||
paymentMethodType: type ?? PaymentType.unknown,
|
||||
customTitle: json['name'] as String? ?? 'Unknown',
|
||||
customIconPath: logos['dark'] as String? ?? 'assets/images/card.png',
|
||||
customDescription: json['description'] as String?);
|
||||
}
|
||||
|
||||
factory PaymentMethod.fromKryptonimJson(Map<String, dynamic> json) {
|
||||
final type = PaymentMethod.getPaymentTypeId(json['payment_method'] as String?);
|
||||
return PaymentMethod(
|
||||
paymentMethodType: type ?? PaymentType.unknown,
|
||||
customTitle: json['payment_method'] as String? ?? 'Unknown',
|
||||
customIconPath: 'assets/images/card.png',
|
||||
);
|
||||
}
|
||||
|
||||
static PaymentType? getPaymentTypeId(String? type) {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'banktransfer':
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/pairs_utils.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.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 RobinhoodBuyProvider extends BuyProvider {
|
||||
RobinhoodBuyProvider({
|
||||
required WalletBase wallet,
|
||||
bool isTestEnvironment = false,
|
||||
HardwareWalletViewModel? hardwareWalletVM,
|
||||
}) : super(
|
||||
wallet: wallet,
|
||||
isTestEnvironment: isTestEnvironment,
|
||||
hardwareWalletVM: hardwareWalletVM,
|
||||
supportedCryptoList: supportedCryptoToFiatPairs(
|
||||
notSupportedCrypto: _notSupportedCrypto, notSupportedFiat: _notSupportedFiat),
|
||||
supportedFiatList: supportedFiatToCryptoPairs(
|
||||
notSupportedFiat: _notSupportedFiat, notSupportedCrypto: _notSupportedCrypto),
|
||||
);
|
||||
|
||||
static const _baseUrl = 'applink.robinhood.com';
|
||||
static const _cIdBaseUrl = 'exchange-helper.such.software';
|
||||
static const _apiBaseUrl = 'api.robinhood.com';
|
||||
static const _assetsPath = '/catpay/v1/supported_currencies/';
|
||||
|
||||
static List<CryptoCurrency> _supportedCrypto = [];
|
||||
static final List<FiatCurrency> _supportedFiat = [FiatCurrency.usd];
|
||||
|
||||
static final List<CryptoCurrency> _notSupportedCrypto = [];
|
||||
|
||||
static final List<FiatCurrency> _notSupportedFiat =
|
||||
FiatCurrency.all.where((fiat) => !_supportedFiat.contains(fiat)).toList();
|
||||
|
||||
@override
|
||||
String get title => 'Robinhood Connect';
|
||||
|
||||
@override
|
||||
String get providerDescription => S.current.robinhood_option_description;
|
||||
|
||||
@override
|
||||
String get lightIcon => 'assets/images/robinhood_light.png';
|
||||
|
||||
@override
|
||||
String get darkIcon => 'assets/images/robinhood_dark.png';
|
||||
|
||||
@override
|
||||
bool get isAggregator => false;
|
||||
|
||||
String get _applicationId => secrets.robinhoodApplicationId;
|
||||
|
||||
String get _apiSecret => secrets.exchangeHelperApiKey;
|
||||
|
||||
Future<List<CryptoCurrency>> getSupportedAssets() async {
|
||||
final uri = Uri.https(_apiBaseUrl, '$_assetsPath', {'applicationId': _applicationId});
|
||||
|
||||
try {
|
||||
final response =
|
||||
await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final pairs = responseData['cryptoCurrencyPairs'] as List<dynamic>;
|
||||
|
||||
final supportedAssets = <CryptoCurrency>[];
|
||||
|
||||
for (final item in pairs) {
|
||||
String code = item['assetCurrency']['code'] as String;
|
||||
if (code == 'AVAX') code = 'AVAXC';
|
||||
try {
|
||||
final currency = CryptoCurrency.fromString(code);
|
||||
supportedAssets.add(currency);
|
||||
} catch (e) {
|
||||
log('Robinhood: Unknown asset code "$code" - skipped');
|
||||
}
|
||||
}
|
||||
return supportedAssets;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
log('Robinhood: Failed to fetch supported assets: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getSignature(String message) async {
|
||||
switch (wallet.type) {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.base:
|
||||
case WalletType.arbitrum:
|
||||
case WalletType.bsc:
|
||||
case WalletType.dogecoin:
|
||||
return wallet.signMessage(message);
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.bitcoinCash:
|
||||
return wallet.signMessage(message, address: wallet.walletAddresses.address);
|
||||
case WalletType.monero:
|
||||
case WalletType.none:
|
||||
case WalletType.nano:
|
||||
case WalletType.banano:
|
||||
case WalletType.wownero:
|
||||
throw Exception("Wallet Type ${wallet.type.name} is not available for Robinhood");
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getConnectId() async {
|
||||
final walletAddress = wallet.walletAddresses.address;
|
||||
final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10;
|
||||
final message = "$_apiSecret:${valid_until}";
|
||||
|
||||
final signature = await getSignature(message);
|
||||
|
||||
final uri = Uri.https(_cIdBaseUrl, "/api/robinhood");
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json
|
||||
.encode({'valid_until': valid_until, 'wallet': walletAddress, 'signature': signature}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return (jsonDecode(response.body) as Map<String, dynamic>)['connectId'] as String;
|
||||
} else {
|
||||
throw Exception('Provider currently unavailable. Status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uri> requestProviderUrl() async {
|
||||
final connectId = await getConnectId();
|
||||
final networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_");
|
||||
|
||||
return Uri.https(_baseUrl, '/u/connect', <String, dynamic>{
|
||||
'applicationId': _applicationId,
|
||||
'connectId': connectId,
|
||||
'walletAddress': wallet.walletAddresses.address,
|
||||
'userIdentifier': wallet.walletAddresses.address,
|
||||
'supportedNetworks': networkName
|
||||
});
|
||||
}
|
||||
|
||||
Future<void>? launchProvider(
|
||||
{required BuildContext context,
|
||||
required Quote quote,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String cryptoCurrencyAddress,
|
||||
String? countryCode}) async {
|
||||
if (wallet.isHardwareWallet) {
|
||||
if (!hardwareWalletVM!.isConnected) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: wallet.walletInfo.type,
|
||||
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, hwwVM) {
|
||||
hwwVM.initWallet(wallet);
|
||||
Navigator.of(context).pop();
|
||||
}));
|
||||
} else {
|
||||
hardwareWalletVM!.initWallet(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final uri = await requestProviderUrl();
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: "Robinhood Connect",
|
||||
alertContent: '${S.of(context).buy_provider_unavailable}: $e',
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Quote>?> fetchQuote(
|
||||
{required CryptoCurrency cryptoCurrency,
|
||||
required FiatCurrency fiatCurrency,
|
||||
required double amount,
|
||||
required bool isBuyAction,
|
||||
required String walletAddress,
|
||||
PaymentType? paymentType,
|
||||
String? customPaymentMethodType,
|
||||
String? countryCode}) async {
|
||||
String? paymentMethod;
|
||||
|
||||
if (_supportedCrypto.isEmpty) _supportedCrypto = await getSupportedAssets();
|
||||
if (_supportedCrypto.isNotEmpty && !(_supportedCrypto.contains(cryptoCurrency))) return null;
|
||||
|
||||
if (paymentType != null && paymentType != PaymentType.all) {
|
||||
paymentMethod = normalizePaymentMethod(paymentType);
|
||||
if (paymentMethod == null) return null;
|
||||
}
|
||||
|
||||
final action = isBuyAction ? 'buy' : 'sell';
|
||||
log('Robinhood: Fetching $action quote: ${isBuyAction ? cryptoCurrency.title : fiatCurrency.name.toUpperCase()} -> ${isBuyAction ? fiatCurrency.name.toUpperCase() : cryptoCurrency.title}, amount: $amount paymentMethod: $paymentMethod');
|
||||
|
||||
final queryParams = {
|
||||
'applicationId': _applicationId,
|
||||
'fiatCode': fiatCurrency.name,
|
||||
'assetCode': cryptoCurrency.title,
|
||||
'fiatAmount': amount.toString(),
|
||||
if (paymentMethod != null) 'paymentMethod': paymentMethod,
|
||||
};
|
||||
|
||||
final uri =
|
||||
Uri.https('api.robinhood.com', '/catpay/v1/${cryptoCurrency.title}/quote/', queryParams);
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: {'accept': 'application/json'});
|
||||
|
||||
final responseData = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
|
||||
final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
|
||||
quote.setFiatCurrency = fiatCurrency;
|
||||
quote.setCryptoCurrency = cryptoCurrency;
|
||||
return [quote];
|
||||
} else {
|
||||
if (responseData.containsKey('message')) {
|
||||
log('Robinhood Error: ${responseData['message']}');
|
||||
} else {
|
||||
printV('Robinhood Failed to fetch $action quote: ${response.statusCode}');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
log('Robinhood: Failed to fetch $action quote: $e');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Supported payment methods:
|
||||
// ● buying_power
|
||||
// ● crypto_balance
|
||||
// ● debit_card
|
||||
// ● bank_transfer
|
||||
}
|
||||
|
||||
String? normalizePaymentMethod(PaymentType paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case PaymentType.creditCard:
|
||||
return 'debit_card';
|
||||
case PaymentType.debitCard:
|
||||
return 'debit_card';
|
||||
case PaymentType.bankTransfer:
|
||||
return 'bank_transfer';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PaymentType _getPaymentTypeByString(String? paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case 'debit_card':
|
||||
return PaymentType.debitCard;
|
||||
case 'bank_transfer':
|
||||
return PaymentType.bankTransfer;
|
||||
default:
|
||||
return PaymentType.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export 'src/auth/cake_pay_welcome_page.dart';
|
||||
export 'src/auth/cake_pay_verify_otp_page.dart';
|
||||
export 'src/auth/cake_pay_account_page.dart';
|
||||
export 'src/cards/cake_pay_cards_page.dart';
|
||||
export 'src/cards/cake_pay_buy_card_page.dart';
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_tile.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class CakePayAccountPage extends BasePage {
|
||||
CakePayAccountPage(this.cakePayAccountViewModel);
|
||||
|
||||
final CakePayAccountViewModel cakePayAccountViewModel;
|
||||
|
||||
@override
|
||||
Widget leading(BuildContext context) {
|
||||
return MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: 37,
|
||||
width: 37,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: S.of(context).seed_alert_back,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: WidgetStateColor.resolveWith(
|
||||
(states) => Colors.transparent),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: backButton(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Text(
|
||||
S.current.account,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.all(24),
|
||||
content: Column(
|
||||
children: [
|
||||
SizedBox(height: 20),
|
||||
Observer(
|
||||
builder: (_) => Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CakePayTile(
|
||||
title: S.of(context).email_address,
|
||||
subTitle: cakePayAccountViewModel.email
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.all(30),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
PrimaryButton(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
text: S.of(context).logout,
|
||||
onPressed: () {
|
||||
cakePayAccountViewModel.logout();
|
||||
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class CakePayVerifyOtpPage extends BasePage {
|
||||
CakePayVerifyOtpPage(this._authViewModel, this._email, this.isSignIn)
|
||||
: _codeController = TextEditingController(),
|
||||
_codeFocus = FocusNode() {
|
||||
_codeController.addListener(() {
|
||||
final otp = _codeController.text;
|
||||
_authViewModel.otp = otp;
|
||||
if (otp.length > 5) {
|
||||
_authViewModel.otpState = CakePayOtpSendEnabled();
|
||||
} else {
|
||||
_authViewModel.otpState = CakePayOtpSendDisabled();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final CakePayAuthViewModel _authViewModel;
|
||||
final bool isSignIn;
|
||||
|
||||
final String _email;
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Text(
|
||||
S.current.verification,
|
||||
style: textMediumSemiBold(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final TextEditingController _codeController;
|
||||
final FocusNode _codeFocus;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
reaction((_) => _authViewModel.otpState, (CakePayOtpState state) {
|
||||
if (state is CakePayOtpFailure) {
|
||||
_onOtpFailure(context, state.error);
|
||||
}
|
||||
if (state is CakePayOtpSuccess) {
|
||||
_onOtpSuccessful(context);
|
||||
}
|
||||
});
|
||||
return KeyboardActions(
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surface,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _codeFocus,
|
||||
toolbarButtons: [(_) => KeyboardDoneButton()],
|
||||
),
|
||||
]),
|
||||
child: Container(
|
||||
height: 0,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.all(24),
|
||||
content: Column(
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
hintText: S.of(context).enter_code,
|
||||
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
focusNode: _codeFocus,
|
||||
controller: _codeController,
|
||||
onSubmit: (_) => _verify(),
|
||||
),
|
||||
SizedBox(height: 14),
|
||||
Text(
|
||||
S.of(context).fill_code,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 34),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(S.of(context).didnt_get_code),
|
||||
SizedBox(width: 20),
|
||||
InkWell(
|
||||
onTap: () => _authViewModel.logIn(_email),
|
||||
child: Text(
|
||||
S.of(context).resend_code,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
text: S.of(context).continue_text,
|
||||
onPressed: _verify,
|
||||
isDisabled: _authViewModel.otpState is CakePayOtpSendDisabled,
|
||||
isLoading: _authViewModel.otpState is CakePayOtpValidating,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onOtpFailure(BuildContext context, String error) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.current.verification,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void _onOtpSuccessful(BuildContext context) =>
|
||||
Navigator.pop(context, true);
|
||||
|
||||
void _verify() async => await _authViewModel.verifyEmail(_codeController.text);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import 'package:hash_wallet/core/email_validator.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class CakePayWelcomePage extends BasePage {
|
||||
CakePayWelcomePage(this._authViewModel)
|
||||
: _formKey = GlobalKey<FormState>(),
|
||||
_emailController = TextEditingController() {
|
||||
_emailController.text = _authViewModel.email;
|
||||
_emailController.addListener(() => _authViewModel.email = _emailController.text);
|
||||
}
|
||||
|
||||
final GlobalKey<FormState> _formKey;
|
||||
|
||||
final CakePayAuthViewModel _authViewModel;
|
||||
|
||||
final TextEditingController _emailController;
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Text(
|
||||
S.current.welcome_to_cakepay,
|
||||
style: textMediumSemiBold(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
reaction((_) => _authViewModel.userVerificationState, (CakePayUserVerificationState state) {
|
||||
if (state is CakePayUserVerificationStateFailure) {
|
||||
_onLoginUserFailure(context, state.error);
|
||||
}
|
||||
if (state is CakePayUserVerificationStateSuccess) {
|
||||
_onLoginSuccessful(context, _authViewModel);
|
||||
}
|
||||
});
|
||||
return ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.all(24),
|
||||
content: Column(
|
||||
children: [
|
||||
SizedBox(height: 90),
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: BaseTextFormField(
|
||||
hintText: S.of(context).email_address,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: EmailValidator(),
|
||||
controller: _emailController,
|
||||
onSubmit: (text) => _login(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
S.of(context).about_cake_pay,
|
||||
style: textLarge(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(S.of(context).cake_pay_account_note,
|
||||
style: textLarge(color: Theme.of(context).colorScheme.onSurface)),
|
||||
],
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.symmetric(vertical: 36, horizontal: 24),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
text: S.of(context).login,
|
||||
onPressed: _login,
|
||||
isLoading:
|
||||
_authViewModel.userVerificationState is CakePayUserVerificationStateLoading,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onLoginUserFailure(BuildContext context, String error) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.current.login,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onLoginSuccessful(BuildContext context, CakePayAuthViewModel authViewModel) async {
|
||||
final verified = await Navigator.pushNamed<bool>(context, Routes.cakePayVerifyOtpPage,
|
||||
arguments: [authViewModel.email, true]);
|
||||
|
||||
if (verified == true && Navigator.of(context).canPop()) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
}
|
||||
|
||||
void _login() async {
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
await _authViewModel.logIn(_emailController.text);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
abstract class CakePayUserVerificationState {}
|
||||
|
||||
class CakePayUserVerificationStateInitial extends CakePayUserVerificationState {}
|
||||
|
||||
class CakePayUserVerificationStateSuccess extends CakePayUserVerificationState {}
|
||||
|
||||
class CakePayUserVerificationStatePending extends CakePayUserVerificationState {}
|
||||
|
||||
class CakePayUserVerificationStateLoading extends CakePayUserVerificationState {}
|
||||
|
||||
class CakePayUserVerificationStateFailure extends CakePayUserVerificationState {
|
||||
CakePayUserVerificationStateFailure({required this.error});
|
||||
|
||||
final String error;
|
||||
}
|
||||
|
||||
abstract class CakePayOtpState {}
|
||||
|
||||
class CakePayOtpValidating extends CakePayOtpState {}
|
||||
|
||||
class CakePayOtpSuccess extends CakePayOtpState {}
|
||||
|
||||
class CakePayOtpSendDisabled extends CakePayOtpState {}
|
||||
|
||||
class CakePayOtpSendEnabled extends CakePayOtpState {}
|
||||
|
||||
class CakePayOtpFailure extends CakePayOtpState {
|
||||
CakePayOtpFailure({required this.error});
|
||||
|
||||
final String error;
|
||||
}
|
||||
|
||||
class CakePayCreateCardState {}
|
||||
|
||||
class CakePayCreateCardStateSuccess extends CakePayCreateCardState {}
|
||||
|
||||
class CakePayCreateCardStateLoading extends CakePayCreateCardState {}
|
||||
|
||||
class CakePayCreateCardStateFailure extends CakePayCreateCardState {
|
||||
CakePayCreateCardStateFailure({required this.error});
|
||||
|
||||
final String error;
|
||||
}
|
||||
|
||||
class UserCakePayCardsState {}
|
||||
|
||||
class UserCakePayCardsStateInitial extends UserCakePayCardsState {}
|
||||
|
||||
class UserCakePayCardsStateNoCards extends UserCakePayCardsState {}
|
||||
|
||||
class UserCakePayCardsStateFetching extends UserCakePayCardsState {}
|
||||
|
||||
class UserCakePayCardsStateFailure extends UserCakePayCardsState {
|
||||
UserCakePayCardsStateFailure({required this.error});
|
||||
|
||||
final String error;
|
||||
}
|
||||
|
||||
class UserCakePayCardsStateSuccess extends UserCakePayCardsState {}
|
||||
|
||||
|
||||
abstract class CakePayVendorState {}
|
||||
|
||||
class InitialCakePayVendorLoadingState extends CakePayVendorState {}
|
||||
|
||||
class CakePayVendorLoadingState extends CakePayVendorState {}
|
||||
|
||||
class CakePayVendorLoadedState extends CakePayVendorState {}
|
||||
@@ -1,800 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/denominations_amount_widget.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/enter_amount_widget.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/image_placeholder.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/link_extractor.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/text_icon_button.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/entities/parsed_address.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/cake_pay_transaction_sent_bottom_sheet.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/output.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class CakePayBuyCardPage extends BasePage {
|
||||
CakePayBuyCardPage(this.cakePayBuyCardViewModel)
|
||||
: _sendViewModel = cakePayBuyCardViewModel.sendViewModel,
|
||||
_amountFieldFocus = FocusNode(),
|
||||
_amountController = TextEditingController(),
|
||||
_quantityFieldFocus = FocusNode(),
|
||||
_quantityController =
|
||||
TextEditingController(text: cakePayBuyCardViewModel.quantity.toString()) {
|
||||
_amountController.addListener(() {
|
||||
cakePayBuyCardViewModel.onAmountChanged(_amountController.text);
|
||||
});
|
||||
}
|
||||
|
||||
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
|
||||
final SendViewModel _sendViewModel;
|
||||
|
||||
bool _effectsInstalled = false;
|
||||
late final BuildContext _overlayCtx;
|
||||
|
||||
@override
|
||||
String get title => cakePayBuyCardViewModel.card.name;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
|
||||
|
||||
@override
|
||||
Widget? trailing(BuildContext context) {
|
||||
return const SizedBox(
|
||||
width: 54,
|
||||
height: 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? middle(BuildContext context) {
|
||||
return Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Lato',
|
||||
color: titleColor(context)),
|
||||
);
|
||||
}
|
||||
|
||||
final TextEditingController _amountController;
|
||||
final FocusNode _amountFieldFocus;
|
||||
final TextEditingController _quantityController;
|
||||
final FocusNode _quantityFieldFocus;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
_setEffects(context);
|
||||
|
||||
final card = cakePayBuyCardViewModel.card;
|
||||
final vendor = cakePayBuyCardViewModel.vendor;
|
||||
|
||||
return Container(
|
||||
color: Colors.transparent,
|
||||
child: KeyboardHideOverlay(
|
||||
child: Column(
|
||||
children: [
|
||||
RoundedOverlayCards(
|
||||
topCardChild: Column(
|
||||
children: [
|
||||
Expanded(flex: 4, child: const SizedBox()),
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withAlpha(150),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2))
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Image.network(
|
||||
card.cardImageUrl ?? '',
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
CakePayCardImagePlaceholder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: const SizedBox()),
|
||||
],
|
||||
),
|
||||
bottomCardChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: card.denominationItems.isNotEmpty
|
||||
? DenominationsAmountWidget(
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
denominations: card.denominationItems,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
quantityFieldFocus: _quantityFieldFocus,
|
||||
quantityController: _quantityController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged,
|
||||
onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel)
|
||||
: EnterAmountWidget(
|
||||
minValue: card.minValue ?? '-',
|
||||
maxValue: card.maxValue ?? '-',
|
||||
fiatCurrency: card.fiatCurrency.title,
|
||||
amountFieldFocus: _amountFieldFocus,
|
||||
amountController: _amountController,
|
||||
onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (vendor.cakeWarnings != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white.withAlpha(50)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
vendor.cakeWarnings!,
|
||||
textAlign: TextAlign.center,
|
||||
style: textSmallSemiBold(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
primary: false,
|
||||
padding: EdgeInsets.zero,
|
||||
child: ClickableLinksText(
|
||||
text: card.description ?? '',
|
||||
textStyle: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
primary: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
if (card.expiryAndValidity != null && card.expiryAndValidity!.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Text(S.of(context).expiry_and_validity + ':',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900)),
|
||||
Expanded(
|
||||
child: Text(card.expiryAndValidity!,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelMedium)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
TextIconButton(
|
||||
label: S.of(context).how_to_use_card,
|
||||
onTap: () => _showHowToUseCard(context, card)),
|
||||
SizedBox(height: 8),
|
||||
TextIconButton(
|
||||
label: S.of(context).settings_terms_and_conditions,
|
||||
onTap: () => _showTermsAndCondition(context, card.termsAndConditions)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Observer(builder: (_) {
|
||||
Widget _buildPaymentMethodWidget(
|
||||
List<CakePayPaymentMethod> methods, CakePayPaymentMethod selected) {
|
||||
return Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 8),
|
||||
child: Text(
|
||||
'Payment Method',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: const SizedBox()),
|
||||
if (methods.length > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6),
|
||||
child: ToggleButtons(
|
||||
isSelected: methods.map((m) => m == selected).toList(),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onPressed: (index) =>
|
||||
cakePayBuyCardViewModel.chooseMethod(methods[index]),
|
||||
children: methods
|
||||
.map((m) => Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Text(m.label),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final methods = cakePayBuyCardViewModel.availableMethods;
|
||||
final selected = cakePayBuyCardViewModel.selectedPaymentMethod ??
|
||||
(methods.isNotEmpty ? methods.first : null);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
methods.length <= 1 || selected == null
|
||||
? const SizedBox.shrink()
|
||||
: _buildPaymentMethodWidget(methods, selected),
|
||||
if (_sendViewModel.walletType == WalletType.litecoin &&
|
||||
_sendViewModel.isMwebEnabled)
|
||||
Observer(
|
||||
builder: (_) => Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
|
||||
child: GestureDetector(
|
||||
key: ValueKey('cake_pay_buy_page_unspent_coin_button_key'),
|
||||
onTap: () {
|
||||
bool value = _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any;
|
||||
_sendViewModel.setAllowMwebCoins(!value);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
StandardCheckbox(
|
||||
caption: S.of(context).litecoin_mweb_allow_coins,
|
||||
captionColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
borderColor: Theme.of(context).colorScheme.primary,
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
value: _sendViewModel.coinTypeToSpendFrom == UnspentCoinType.any,
|
||||
onChanged: (bool? value) {
|
||||
_sendViewModel.setAllowMwebCoins(value ?? false);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (FeatureFlag.hasDevOptions && FeatureFlag.isCakePayPurchaseSimulationEnabled)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 0, right: 20, left: 20),
|
||||
child: LoadingPrimaryButton(
|
||||
onPressed: () {
|
||||
//Request dummy node to get the focus out of the text fields
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = true;
|
||||
isIOSUnavailable(card)
|
||||
? alertIOSAvailability(context, card)
|
||||
: confirmPurchaseFirst(context);
|
||||
},
|
||||
text: '(Dev) Simulate Purchasing Gift Card',
|
||||
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
|
||||
cakePayBuyCardViewModel.isPurchasing,
|
||||
isLoading: _sendViewModel.state is IsExecutingState ||
|
||||
cakePayBuyCardViewModel.isPurchasing,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 34, right: 20, left: 20),
|
||||
child: LoadingPrimaryButton(
|
||||
onPressed: () {
|
||||
//Request dummy node to get the focus out of the text fields
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
|
||||
isIOSUnavailable(card)
|
||||
? alertIOSAvailability(context, card)
|
||||
: confirmPurchaseFirst(context);
|
||||
},
|
||||
text: S.of(context).purchase_gift_card,
|
||||
isDisabled: !cakePayBuyCardViewModel.isAmountSufficient ||
|
||||
cakePayBuyCardViewModel.isPurchasing || _sendViewModel.state is ExecutedSuccessfullyState,
|
||||
isLoading: _sendViewModel.state is IsExecutingState ||
|
||||
cakePayBuyCardViewModel.isPurchasing,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isWordInCardsName(CakePayCard card, String word) {
|
||||
return card.name.toLowerCase().contains(word.toLowerCase());
|
||||
}
|
||||
|
||||
bool isIOSUnavailable(CakePayCard card) {
|
||||
if (!Platform.isIOS && !Platform.isMacOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final isDigitalGameStores = isWordInCardsName(card, 'playstation') ||
|
||||
isWordInCardsName(card, 'xbox') ||
|
||||
isWordInCardsName(card, 'steam') ||
|
||||
isWordInCardsName(card, 'meta quest') ||
|
||||
isWordInCardsName(card, 'kigso') ||
|
||||
isWordInCardsName(card, 'game world') ||
|
||||
isWordInCardsName(card, 'google') ||
|
||||
isWordInCardsName(card, 'nintendo');
|
||||
final isGCodes = isWordInCardsName(card, 'gcodes');
|
||||
final isApple = isWordInCardsName(card, 'itunes') || isWordInCardsName(card, 'apple');
|
||||
final isTidal = isWordInCardsName(card, 'tidal');
|
||||
final isVPNServices = isWordInCardsName(card, 'nordvpn') ||
|
||||
isWordInCardsName(card, 'expressvpn') ||
|
||||
isWordInCardsName(card, 'surfshark') ||
|
||||
isWordInCardsName(card, 'proton');
|
||||
final isStreamingServices = isWordInCardsName(card, 'netflix') ||
|
||||
isWordInCardsName(card, 'spotify') ||
|
||||
isWordInCardsName(card, 'hulu') ||
|
||||
isWordInCardsName(card, 'hbo') ||
|
||||
isWordInCardsName(card, 'soundcloud') ||
|
||||
isWordInCardsName(card, 'twitch');
|
||||
final isDatingServices = isWordInCardsName(card, 'tinder');
|
||||
|
||||
return isDigitalGameStores ||
|
||||
isGCodes ||
|
||||
isApple ||
|
||||
isTidal ||
|
||||
isVPNServices ||
|
||||
isStreamingServices ||
|
||||
isDatingServices;
|
||||
}
|
||||
|
||||
Future<void> alertIOSAvailability(BuildContext context, CakePayCard card) async {
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
return await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: S.of(context).cakepay_ios_not_available,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
// _walletHardwareRestoreVM.error = null;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> confirmPurchaseFirst(BuildContext context) async {
|
||||
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
|
||||
if (!isLogged) {
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
|
||||
} else {
|
||||
cakePayBuyCardViewModel.isPurchasing = true;
|
||||
await _showconfirmPurchaseFirstAlert(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showconfirmPurchaseFirstAlert(BuildContext context) async {
|
||||
if (!cakePayBuyCardViewModel.confirmsNoVpn ||
|
||||
!cakePayBuyCardViewModel.confirmsVoidedRefund ||
|
||||
!cakePayBuyCardViewModel.confirmsTermsAgreed) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => ThreeCheckboxAlert(
|
||||
alertTitle: S.of(context).cakepay_confirm_purchase,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).confirm,
|
||||
actionLeftButton: () {
|
||||
cakePayBuyCardViewModel.isPurchasing = false;
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
actionRightButton: (confirmsNoVpn, confirmsVoidedRefund, confirmsTermsAgreed) {
|
||||
cakePayBuyCardViewModel.confirmsNoVpn = confirmsNoVpn;
|
||||
cakePayBuyCardViewModel.confirmsVoidedRefund = confirmsVoidedRefund;
|
||||
cakePayBuyCardViewModel.confirmsTermsAgreed = confirmsTermsAgreed;
|
||||
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (cakePayBuyCardViewModel.confirmsNoVpn &&
|
||||
cakePayBuyCardViewModel.confirmsVoidedRefund &&
|
||||
cakePayBuyCardViewModel.confirmsTermsAgreed) {
|
||||
await purchaseCard(context);
|
||||
} else {
|
||||
cakePayBuyCardViewModel.isPurchasing = false;
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> purchaseCard(BuildContext context) async {
|
||||
bool isLogged = await cakePayBuyCardViewModel.isUserLogged;
|
||||
if (!isLogged) {
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
Navigator.of(context).pushNamed(Routes.cakePayWelcomePage);
|
||||
} else {
|
||||
try {
|
||||
await cakePayBuyCardViewModel.createOrder();
|
||||
} catch (_) {
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
await cakePayBuyCardViewModel.logout();
|
||||
}
|
||||
}
|
||||
cakePayBuyCardViewModel.isPurchasing = false;
|
||||
}
|
||||
|
||||
BuildContext? dialogContext;
|
||||
BuildContext? loadingBottomSheetContext;
|
||||
BuildContext? confirmBottomSheetContext;
|
||||
|
||||
void _setEffects(BuildContext context) {
|
||||
if (_effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_overlayCtx = Navigator.of(context).context;
|
||||
|
||||
if (_sendViewModel.isElectrumWallet) {
|
||||
bitcoin!.updateFeeRates(_sendViewModel.wallet);
|
||||
}
|
||||
|
||||
reaction((_) => _sendViewModel.state, (ExecutionState state) async {
|
||||
if (dialogContext != null && dialogContext!.mounted) Navigator.of(dialogContext!).pop();
|
||||
|
||||
if (confirmBottomSheetContext != null && confirmBottomSheetContext!.mounted) {
|
||||
Navigator.of(confirmBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
if (state is! IsExecutingState &&
|
||||
loadingBottomSheetContext != null &&
|
||||
loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted)
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('cake_pay_buy_page_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('cake_pay_buy_page_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsExecutingState) {
|
||||
// wait a bit to avoid showing the loading dialog if transaction is failed
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final currentState = _sendViewModel.state;
|
||||
if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).generating_transaction,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
if (cakePayBuyCardViewModel.order == null) return;
|
||||
|
||||
ReactionDisposer? disposer;
|
||||
|
||||
disposer = reaction((_) => cakePayBuyCardViewModel.isOrderExpired, (bool isExpired) {
|
||||
if (isExpired) {
|
||||
_sendViewModel.state = FailureState('Order expired');
|
||||
disposer?.call();
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (context.mounted) {
|
||||
final order = cakePayBuyCardViewModel.order;
|
||||
|
||||
final displayingOutputs = _sendViewModel.outputs
|
||||
.map((o) => o.OutputCopyWithParsedAddress(
|
||||
parsedAddress: ParsedAddress(
|
||||
addresses: [o.address],
|
||||
name: 'Cake Pay',
|
||||
profileName: order?.cards.first.cardName ?? 'Cake Pay',
|
||||
profileImageUrl: order?.cards.first.cardImagePath ?? '',
|
||||
),
|
||||
fiatAmount: '${order?.totalReceiveAmount}',
|
||||
))
|
||||
.toList();
|
||||
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
confirmBottomSheetContext = bottomSheetContext;
|
||||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('cake_pay_buy_page_confirm_sending_dialog_key'),
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
cakePayBuyCardViewModel: cakePayBuyCardViewModel,
|
||||
paymentId: S.of(bottomSheetContext).payment_id,
|
||||
paymentIdValue: cakePayBuyCardViewModel.order?.orderId,
|
||||
expirationTime: cakePayBuyCardViewModel.formattedRemainingTime,
|
||||
walletType: _sendViewModel.walletType,
|
||||
titleIconPath: _sendViewModel.selectedCryptoCurrency.iconPath,
|
||||
currency: _sendViewModel.selectedCryptoCurrency,
|
||||
amount: S.of(bottomSheetContext).send_amount,
|
||||
amountValue: _sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
|
||||
_sendViewModel.pendingTransaction!.amountFormatted,
|
||||
_sendViewModel.selectedCryptoCurrency),
|
||||
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
|
||||
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
|
||||
fee: S.of(bottomSheetContext).send_fee,
|
||||
feeValue: "${_sendViewModel.amountParsingProxy.getDisplayCryptoAmount(
|
||||
_sendViewModel.pendingTransaction!.feeFormattedValue,
|
||||
_sendViewModel.selectedCryptoCurrency)} ${_sendViewModel.amountParsingProxy
|
||||
.getCryptoSymbol(_sendViewModel.wallet.currency)}",
|
||||
feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: displayingOutputs,
|
||||
footerType: FooterType.slideActionButton,
|
||||
isSlideActionEnabled: _sendViewModel.isReadyForSend,
|
||||
slideActionButtonText:
|
||||
cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send',
|
||||
accessibleNavigationModeSlideActionButtonText:
|
||||
cakePayBuyCardViewModel.isSimulating ? 'Simulate' : S.of(context).send,
|
||||
onSlideActionComplete: () async {
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
cakePayBuyCardViewModel.isSimulating
|
||||
? cakePayBuyCardViewModel.simulatePayment()
|
||||
: _sendViewModel.commitTransaction(context);
|
||||
},
|
||||
change: _sendViewModel.pendingTransaction!.change,
|
||||
isOpenCryptoPay: _sendViewModel.ocpRequest != null,
|
||||
amountParsingProxy: _sendViewModel.amountParsingProxy,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
confirmBottomSheetContext = null;
|
||||
cakePayBuyCardViewModel.isSimulatingFlow = false;
|
||||
_handleDispose(disposer);
|
||||
if (result == null) _sendViewModel.dismissTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
final order = cakePayBuyCardViewModel.order;
|
||||
final outputsCopy = List<Output>.from(_sendViewModel.outputs);
|
||||
|
||||
final displayingOutputs = outputsCopy
|
||||
.map((o) => o.OutputCopyWithParsedAddress(
|
||||
parsedAddress: ParsedAddress(
|
||||
addresses: [o.address],
|
||||
name: 'Cake Pay',
|
||||
profileName: order?.cards.first.cardName ?? 'Cake Pay',
|
||||
profileImageUrl: order?.cards.first.cardImagePath ?? '',
|
||||
),
|
||||
fiatAmount: '${order?.totalReceiveAmount}',
|
||||
))
|
||||
.toList();
|
||||
|
||||
_sendViewModel.clearOutputs();
|
||||
|
||||
final bool usePageContextLater = context.mounted;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (displayingOutputs.isEmpty) {
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
final BuildContext sheetParentCtx = usePageContextLater ? context : _overlayCtx;
|
||||
|
||||
if (!sheetParentCtx.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: sheetParentCtx,
|
||||
useRootNavigator: !usePageContextLater,
|
||||
isScrollControlled: true,
|
||||
isDismissible: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (sheetCtx) {
|
||||
return CakePayTransactionSentBottomSheet(
|
||||
key: const ValueKey('cake_pay_buy_page_transaction_sent_bottom_sheet_key'),
|
||||
titleText: S.of(sheetCtx).transaction_sent,
|
||||
titleIconWidget: const CircleAvatar(
|
||||
radius: 10,
|
||||
backgroundColor: Colors.green,
|
||||
child: Icon(Icons.check, size: 16, color: Colors.white),
|
||||
),
|
||||
output: displayingOutputs.first,
|
||||
currency: _sendViewModel.selectedCryptoCurrency,
|
||||
amount: S.of(sheetCtx).send_amount,
|
||||
amountValue: _sendViewModel.pendingTransaction!.amountFormatted,
|
||||
quantity: 'QTY: ${cakePayBuyCardViewModel.quantity}',
|
||||
fiatAmountValue: _sendViewModel.pendingTransactionFiatAmountFormatted,
|
||||
fee: S.of(sheetCtx).send_fee,
|
||||
feeValue: _sendViewModel.pendingTransaction!.feeFormatted,
|
||||
feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
|
||||
paymentId: 'Order ID',
|
||||
paymentIdValue: order?.orderId ?? '',
|
||||
onClose: () {
|
||||
Navigator.of(sheetCtx).pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsAwaitingDeviceResponseState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext bottomSheetContext) => InfoBottomSheet(
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).proceed_on_device,
|
||||
contentImage: 'assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
contentImageColor: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
content: S.of(bottomSheetContext).proceed_on_device_description,
|
||||
singleActionButtonText: S.of(context).cancel,
|
||||
onSingleActionButtonPressed: () {
|
||||
_sendViewModel.state = InitialExecutionState();
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_effectsInstalled = true;
|
||||
}
|
||||
|
||||
void _handleDispose(ReactionDisposer? disposer) {
|
||||
cakePayBuyCardViewModel.dispose();
|
||||
if (disposer != null) {
|
||||
disposer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showHowToUseCard(BuildContext context, CakePayCard card) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CakePayAlertModal(
|
||||
title: S.of(context).how_to_use_card,
|
||||
dismissible: true,
|
||||
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(card.name, style: Theme.of(context).textTheme.headlineSmall)),
|
||||
ClickableLinksText(
|
||||
text: card.howToUse ?? '',
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!,
|
||||
linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))
|
||||
]),
|
||||
actionTitle: S.current.got_it,
|
||||
showCloseButton: false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _showTermsAndCondition(BuildContext context, String? termsAndConditions) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CakePayAlertModal(
|
||||
title: S.of(context).settings_terms_and_conditions,
|
||||
dismissible: true,
|
||||
content: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: ClickableLinksText(
|
||||
text: termsAndConditions ?? '',
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!,
|
||||
linkStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary, fontStyle: FontStyle.italic))),
|
||||
actionTitle: S.of(context).agree,
|
||||
showCloseButton: false,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_search_bar_widget.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/user_card_item.dart';
|
||||
import 'package:hash_wallet/entities/country.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/card_item.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/filter_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/cake_pay_card_info_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:hash_wallet/src/widgets/picker.dart';
|
||||
import 'package:hash_wallet/src/widgets/tab_view_wrapper_widget.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class CakePayCardsPage extends BasePage {
|
||||
CakePayCardsPage(this._cardsListViewModel);
|
||||
|
||||
final CakePayCardsListViewModel _cardsListViewModel;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Text(
|
||||
'Cake Pay',
|
||||
style: textMediumSemiBold(
|
||||
color: titleColor(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
return Observer(builder: (_) {
|
||||
final loggedIn = _cardsListViewModel.isUserAuthenticated == true;
|
||||
|
||||
Future<void> _handleOnPressed() async {
|
||||
if (loggedIn) {
|
||||
Navigator.pushNamed(context, Routes.cakePayAccountPage);
|
||||
return;
|
||||
}
|
||||
final success = await Navigator.pushNamed<bool>(context, Routes.cakePayWelcomePage);
|
||||
|
||||
if (success == true) await _cardsListViewModel.checkAuth();
|
||||
}
|
||||
|
||||
if (!loggedIn || _cardsListViewModel.username == null) {
|
||||
return _TrailingIcon(
|
||||
asset: 'assets/images/profile.png',
|
||||
iconColor: pageIconColor(context) ?? Colors.white,
|
||||
onPressed: () async => await _handleOnPressed(),
|
||||
);
|
||||
}
|
||||
final letter = _cardsListViewModel.username!.trim()[0].toUpperCase();
|
||||
return IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: 25,
|
||||
onPressed: _handleOnPressed,
|
||||
icon: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Text(
|
||||
letter,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return KeyboardHideOverlay(child:CakePayCardsPageBody(cardsListViewModel: _cardsListViewModel, titleColor: titleColor));
|
||||
}
|
||||
}
|
||||
|
||||
class CakePayCardsPageBody extends StatefulWidget {
|
||||
const CakePayCardsPageBody({
|
||||
super.key,
|
||||
required CakePayCardsListViewModel cardsListViewModel,
|
||||
required this.titleColor,
|
||||
}) : _cardsListViewModel = cardsListViewModel;
|
||||
|
||||
final CakePayCardsListViewModel _cardsListViewModel;
|
||||
final Color? Function(BuildContext) titleColor;
|
||||
|
||||
@override
|
||||
State<CakePayCardsPageBody> createState() => _CakePayCardsPageBodyState();
|
||||
}
|
||||
|
||||
class _CakePayCardsPageBodyState extends State<CakePayCardsPageBody> {
|
||||
ReactionDisposer? _countryPickerDisposer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final viewModel = widget._cardsListViewModel;
|
||||
|
||||
_countryPickerDisposer = when(
|
||||
(_) => viewModel.shouldShowCountryPicker,
|
||||
() async {
|
||||
viewModel.storeInitialFilterStates();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) async {
|
||||
await showCountryPicker(context, viewModel);
|
||||
if (viewModel.hasFiltersChanged) {
|
||||
viewModel.resetLoadingNextPageState();
|
||||
viewModel.getVendors();
|
||||
}
|
||||
viewModel.settingsStore.selectedCakePayCountry = viewModel.selectedCountry;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_countryPickerDisposer?.call();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (_) {
|
||||
final isUserAuthenticated = widget._cardsListViewModel.isUserAuthenticated;
|
||||
|
||||
if (isUserAuthenticated == null) return const _Loading();
|
||||
|
||||
if (isUserAuthenticated == false || !FeatureFlag.isCakePayRedemptionFlowEnabled) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: _ShopTab(cardsListViewModel: widget._cardsListViewModel),
|
||||
);
|
||||
}
|
||||
|
||||
final titleColor = widget.titleColor(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: TabViewWrapper(
|
||||
labelStyle: TextStyle(
|
||||
color: titleColor,
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: titleColor?.withAlpha(150) ?? Colors.white70,
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w400),
|
||||
indicatorColor: titleColor,
|
||||
tabs: const [
|
||||
Tab(text: 'My Cards'),
|
||||
Tab(text: 'Shop')
|
||||
],
|
||||
views: [
|
||||
_MyCardsTab(
|
||||
cardsListViewModel: widget._cardsListViewModel,
|
||||
),
|
||||
_ShopTab(cardsListViewModel: widget._cardsListViewModel)
|
||||
]),
|
||||
)
|
||||
]));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showFilterWidget(
|
||||
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
|
||||
return showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return FilterWidget(filterItems: cardsListViewModel.createFilterItems);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showCountryPicker(
|
||||
BuildContext context, CakePayCardsListViewModel cardsListViewModel) async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => Picker(
|
||||
title: S.of(context).select_your_country,
|
||||
items: cardsListViewModel.availableCountries,
|
||||
images: cardsListViewModel.availableCountries
|
||||
.map((e) => Image.asset(
|
||||
e.iconPath,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
width: 58,
|
||||
height: 58,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
selectedAtIndex:
|
||||
cardsListViewModel.availableCountries.indexOf(cardsListViewModel.selectedCountry),
|
||||
onItemSelected: (Country country) => cardsListViewModel.setSelectedCountry(country),
|
||||
isSeparated: false,
|
||||
hintText: S.of(context).search,
|
||||
matchingCriteria: (Country country, String searchText) =>
|
||||
country.fullName.toLowerCase().contains(searchText.toLowerCase())));
|
||||
}
|
||||
|
||||
class _TrailingIcon extends StatelessWidget {
|
||||
const _TrailingIcon({required this.asset, this.onPressed, required this.iconColor});
|
||||
|
||||
final String asset;
|
||||
final VoidCallback? onPressed;
|
||||
final Color iconColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
label: S.of(context).profile,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
highlightColor: Colors.transparent,
|
||||
onPressed: onPressed,
|
||||
icon: ImageIcon(AssetImage(asset), size: 25, color: iconColor),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _MyCardsTab extends StatefulWidget {
|
||||
const _MyCardsTab({required this.cardsListViewModel});
|
||||
|
||||
final CakePayCardsListViewModel cardsListViewModel;
|
||||
|
||||
@override
|
||||
State<_MyCardsTab> createState() => _MyCardsTabState();
|
||||
}
|
||||
|
||||
class _MyCardsTabState extends State<_MyCardsTab> {
|
||||
late final TextEditingController _searchController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_searchController = TextEditingController(
|
||||
text: widget.cardsListViewModel.searchMyCardsString,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = widget.cardsListViewModel;
|
||||
return Observer(builder: (_) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
|
||||
child: CakePaySearchBar(
|
||||
initialQuery: viewModel.searchMyCardsString,
|
||||
controller: _searchController,
|
||||
onSearch: viewModel.setMyCardsQuery,
|
||||
onFilter: () async {}, // TODO: implement filter
|
||||
)),
|
||||
Expanded(
|
||||
child: Observer(builder: (_) {
|
||||
final cards = viewModel.filteredUserCards;
|
||||
if (viewModel.userCardState is UserCakePayCardsStateFetching) return const _Loading();
|
||||
if (viewModel.userCardState is UserCakePayCardsStateNoCards)
|
||||
return Center(child: Text(S.of(context).no_cards_found));
|
||||
|
||||
final showThumb = cards.length > 6;
|
||||
final userCardsList = Stack(
|
||||
children: [
|
||||
GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
childAspectRatio: 1.25,
|
||||
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 3 : 2,
|
||||
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
|
||||
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
|
||||
padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
|
||||
itemCount: cards.length,
|
||||
itemBuilder: (_, i) {
|
||||
final card = cards[i];
|
||||
return UserCardItem(
|
||||
logoUrl: card.cardImageUrl,
|
||||
title: card.name,
|
||||
subTitle: '\$100',
|
||||
onTap: () => _showCardInfoBottomSheet(context, card),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return showThumb
|
||||
? Scrollbar(
|
||||
key: ValueKey('cake_pay_my_cards_tab_scrollbar_key'),
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
child: userCardsList,
|
||||
)
|
||||
: userCardsList;
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showCardInfoBottomSheet(BuildContext context, CakePayCard card) async {
|
||||
bool isReloadable = false; // TODO: replace with real logic
|
||||
if (card.name.toLowerCase().contains('prepaid')) {
|
||||
isReloadable = true;
|
||||
}
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return isReloadable
|
||||
? CakePayCardInfoBottomSheet(
|
||||
isReloadable: isReloadable,
|
||||
titleText: 'Reloadable Card',
|
||||
balance: '100 USD',
|
||||
howToUse: card.howToUse,
|
||||
footerType: FooterType.doubleActionButton,
|
||||
applyBoxShadow: true,
|
||||
contentImage: card.cardImageUrl,
|
||||
leftActionButtonKey: const Key('cake_pay_cards_page_reload_card_left_button_key'),
|
||||
doubleActionLeftButtonText: 'Archive',
|
||||
onLeftActionButtonPressed: () {},
|
||||
rightActionButtonKey: const Key('cake_pay_cards_page_reload_card_right_button_key'),
|
||||
doubleActionRightButtonText: 'Top Up',
|
||||
onRightActionButtonPressed: () {},
|
||||
onUpdateBalancePressed: () {})
|
||||
: CakePayCardInfoBottomSheet(
|
||||
isReloadable: isReloadable,
|
||||
titleText: card.name,
|
||||
balance: '500 USD',
|
||||
howToUse: card.howToUse,
|
||||
footerType: FooterType.singleActionButton,
|
||||
applyBoxShadow: true,
|
||||
contentImage: card.cardImageUrl,
|
||||
singleActionButtonKey: const Key('cake_pay_cards_page_card_info_bottom_sheet_key'),
|
||||
singleActionButtonText: 'Mark As Used',
|
||||
onSingleActionButtonPressed: () {},
|
||||
onUpdateBalancePressed: () {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class _ShopTab extends StatefulWidget {
|
||||
const _ShopTab({required this.cardsListViewModel});
|
||||
|
||||
final CakePayCardsListViewModel cardsListViewModel;
|
||||
|
||||
@override
|
||||
State<_ShopTab> createState() => _ShopTabState();
|
||||
}
|
||||
|
||||
class _ShopTabState extends State<_ShopTab> {
|
||||
late final ScrollController _scroll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scroll = ScrollController()
|
||||
..addListener(() {
|
||||
if (!_scroll.hasClients) return;
|
||||
final max = _scroll.position.maxScrollExtent;
|
||||
|
||||
final threshold = 200.0;
|
||||
if (_scroll.offset >= max - threshold && !_scroll.position.outOfRange) {
|
||||
widget.cardsListViewModel.fetchNextPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scroll.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewModel = widget.cardsListViewModel;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(2, 6, 0, 6),
|
||||
child: CakePaySearchBar(
|
||||
initialQuery: viewModel.searchString,
|
||||
onSearch: (String searchText) {
|
||||
if (searchText != viewModel.searchString) {
|
||||
viewModel.searchString = searchText;
|
||||
viewModel.resetLoadingNextPageState();
|
||||
viewModel.getVendors(text: searchText);
|
||||
}
|
||||
},
|
||||
onFilter: () async {
|
||||
viewModel.storeInitialFilterStates();
|
||||
await showFilterWidget(context, viewModel);
|
||||
if (viewModel.hasFiltersChanged) {
|
||||
viewModel.resetLoadingNextPageState();
|
||||
viewModel.getVendors(text: viewModel.searchString);
|
||||
}
|
||||
},
|
||||
onCountryPick: () async {
|
||||
viewModel.storeInitialFilterStates();
|
||||
await showCountryPicker(context, viewModel);
|
||||
if (viewModel.hasFiltersChanged) {
|
||||
viewModel.resetLoadingNextPageState();
|
||||
viewModel.getVendors(text: viewModel.searchString);
|
||||
}
|
||||
},
|
||||
selectedCountry: viewModel.selectedCountry,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(builder: (_) {
|
||||
final vendors = viewModel.cakePayVendors;
|
||||
|
||||
if (viewModel.vendorsState is! CakePayVendorLoadedState) {
|
||||
return const _Loading();
|
||||
}
|
||||
|
||||
if (vendors.isEmpty) return Center(child: Text(S.of(context).no_cards_found));
|
||||
|
||||
final loadingMore = viewModel.isLoadingNextPage;
|
||||
final showThumb = vendors.length > 3;
|
||||
final cardsList = Stack(
|
||||
children: [
|
||||
GridView.builder(
|
||||
controller: _scroll,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: responsiveLayoutUtil.shouldRenderTabletUI ? 2 : 1,
|
||||
childAspectRatio: 5,
|
||||
crossAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5,
|
||||
mainAxisSpacing: responsiveLayoutUtil.shouldRenderTabletUI ? 10 : 5),
|
||||
padding: EdgeInsets.only(left: 2, right: showThumb ? 10 : 22),
|
||||
itemCount: vendors.length + (loadingMore ? 1 : 0),
|
||||
itemBuilder: (_, i) {
|
||||
if (i >= vendors.length) return const _Loading();
|
||||
final vendor = vendors[i];
|
||||
return CardItem(
|
||||
logoUrl: vendor.card?.cardImageUrl,
|
||||
title: vendor.name,
|
||||
subTitle: vendor.card?.description ?? '',
|
||||
onTap: () => Navigator.pushNamed(context, Routes.cakePayBuyCardPage,
|
||||
arguments: [vendor]));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
return showThumb
|
||||
? Scrollbar(
|
||||
key: ValueKey('cake_pay_shop_tab_scrollbar_key'),
|
||||
thumbVisibility: true,
|
||||
trackVisibility: true,
|
||||
controller: _scroll,
|
||||
child: cardsList,
|
||||
)
|
||||
: cardsList;
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Loading extends StatelessWidget {
|
||||
const _Loading();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
|
||||
class CakePayCard {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? termsAndConditions;
|
||||
final String? howToUse;
|
||||
final String? expiryAndValidity;
|
||||
final String? cardImageUrl;
|
||||
final String? country;
|
||||
final FiatCurrency fiatCurrency;
|
||||
final String? minValueUsd;
|
||||
final String? maxValueUsd;
|
||||
final String? minValue;
|
||||
final String? maxValue;
|
||||
final List<Denomination> denominationItems;
|
||||
|
||||
CakePayCard({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.termsAndConditions,
|
||||
this.howToUse,
|
||||
this.expiryAndValidity,
|
||||
this.cardImageUrl,
|
||||
this.country,
|
||||
required this.fiatCurrency,
|
||||
this.minValueUsd,
|
||||
this.maxValueUsd,
|
||||
this.minValue,
|
||||
this.maxValue,
|
||||
List<Denomination>? denominationItems,
|
||||
}) : denominationItems = denominationItems ?? const [];
|
||||
|
||||
factory CakePayCard.fromJson(Map<String, dynamic> json) {
|
||||
final name = stripHtmlIfNeeded(json['name'] as String? ?? '');
|
||||
final description = stripHtmlIfNeeded(json['description'] as String? ?? '');
|
||||
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
|
||||
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
|
||||
final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? '');
|
||||
|
||||
String? minValue = json['min_value'] as String?;
|
||||
|
||||
final parsedMinValueLocal = _toDouble(json['min_value']);
|
||||
final parsedMinValueUsd = _toDouble(json['min_value_usd']);
|
||||
|
||||
if (parsedMinValueLocal != null && parsedMinValueLocal > 0 && parsedMinValueUsd != null && parsedMinValueUsd > 0 && parsedMinValueUsd < 10.0) {
|
||||
final rate = parsedMinValueLocal / parsedMinValueUsd;
|
||||
final minLocalValueLimit = 10.0 * rate;
|
||||
minValue = minLocalValueLimit.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
final raw = (json['denominations'] as List?) ?? const [];
|
||||
final denominations = <Denomination>[];
|
||||
for (final item in raw) {
|
||||
if (item is Map) {
|
||||
denominations.add(Denomination.fromJson(Map<String, dynamic>.from(item)));
|
||||
}
|
||||
}
|
||||
|
||||
return CakePayCard(
|
||||
id: json['id'] as int? ?? 0,
|
||||
name: name,
|
||||
description: description,
|
||||
termsAndConditions: termsAndConditions,
|
||||
howToUse: howToUse,
|
||||
expiryAndValidity: json['expiry_and_validity'] as String?,
|
||||
cardImageUrl: json['card_image_url'] as String?,
|
||||
country: json['country'] as String?,
|
||||
fiatCurrency: fiatCurrency,
|
||||
minValueUsd: json['min_value_usd'] as String?,
|
||||
maxValueUsd: json['max_value_usd'] as String?,
|
||||
minValue: minValue,
|
||||
maxValue: json['max_value'] as String?,
|
||||
denominationItems: denominations,
|
||||
);
|
||||
}
|
||||
|
||||
static String stripHtmlIfNeeded(String text) {
|
||||
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
|
||||
}
|
||||
static double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
class Denomination {
|
||||
final double value;
|
||||
final int? cardId;
|
||||
final double? usdValue;
|
||||
|
||||
Denomination({
|
||||
required this.value,
|
||||
required this.cardId,
|
||||
this.usdValue,
|
||||
});
|
||||
|
||||
static double? _toDouble(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse(v.toString());
|
||||
}
|
||||
|
||||
factory Denomination.fromJson(Map<String, dynamic> json) {
|
||||
return Denomination(
|
||||
value: _toDouble(json['value']) ?? 0,
|
||||
cardId: json['card_id'] is int ? json['card_id'] as int : int.tryParse('${json['card_id']}'),
|
||||
usdValue: _toDouble(json['usd_value']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB, ZEC }
|
||||
|
||||
extension CakePayPaymentMethodLabel on CakePayPaymentMethod {
|
||||
String get label => switch (this) {
|
||||
CakePayPaymentMethod.BTC => 'Bitcoin',
|
||||
CakePayPaymentMethod.BTC_LN => 'Lightning',
|
||||
CakePayPaymentMethod.XMR => 'Monero',
|
||||
CakePayPaymentMethod.LTC => 'Litecoin',
|
||||
CakePayPaymentMethod.LTC_MWEB => 'MWEB',
|
||||
CakePayPaymentMethod.ZEC => 'Zcash',
|
||||
};
|
||||
}
|
||||
|
||||
class CakePayOrder {
|
||||
final String orderId;
|
||||
final List<OrderCard> cards;
|
||||
final String? externalId;
|
||||
final double amountUsd;
|
||||
final String totalReceiveAmount;
|
||||
final int quantity;
|
||||
final String status;
|
||||
final String? vouchers;
|
||||
final String fiatCurrencyCode;
|
||||
final PaymentData paymentData;
|
||||
|
||||
CakePayOrder({
|
||||
required this.orderId,
|
||||
required this.cards,
|
||||
required this.externalId,
|
||||
required this.amountUsd,
|
||||
required this.totalReceiveAmount,
|
||||
required this.quantity,
|
||||
required this.status,
|
||||
required this.vouchers,
|
||||
required this.fiatCurrencyCode,
|
||||
required this.paymentData,
|
||||
});
|
||||
|
||||
factory CakePayOrder.fromMap(Map<String, dynamic> map) {
|
||||
|
||||
final cards = map['cards'] as List<dynamic>;
|
||||
final firstCard = cards.isNotEmpty ? cards.first as Map<String, dynamic> : {};
|
||||
|
||||
return CakePayOrder(
|
||||
orderId: map['order_id'] as String,
|
||||
cards: cards
|
||||
.map((x) => OrderCard.fromMap(x as Map<String, dynamic>))
|
||||
.toList(),
|
||||
externalId: map['external_id'] as String?,
|
||||
amountUsd: map['amount_usd'] as double,
|
||||
totalReceiveAmount: firstCard['subtotal'] as String? ?? '',
|
||||
quantity: firstCard['quantity'] as int,
|
||||
status: map['status'] as String,
|
||||
vouchers: map['vouchers'] as String?,
|
||||
fiatCurrencyCode: firstCard['currency_code'] as String? ?? '',
|
||||
paymentData: PaymentData.fromMap(map['payment_data'] as Map<String, dynamic>));
|
||||
}
|
||||
|
||||
static CryptoPaymentData? getPaymentDataFor({CakePayPaymentMethod? method,CakePayOrder? order}) {
|
||||
if (order == null || method == null) return null;
|
||||
|
||||
final data = switch (method) {
|
||||
CakePayPaymentMethod.BTC => order.paymentData.btc,
|
||||
CakePayPaymentMethod.XMR => order.paymentData.xmr,
|
||||
CakePayPaymentMethod.LTC => order.paymentData.ltc,
|
||||
CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb,
|
||||
CakePayPaymentMethod.ZEC => order.paymentData.zec,
|
||||
CakePayPaymentMethod.BTC_LN => order.paymentData.btc_ln,
|
||||
};
|
||||
|
||||
if (data == null) return null;
|
||||
|
||||
final bip21 = data.paymentUrls?.bip21;
|
||||
if (bip21 != null && bip21.isNotEmpty) {
|
||||
final uri = Uri.parse(bip21);
|
||||
final addr = uri.path;
|
||||
final price = uri.queryParameters['amount'] ?? data.price;
|
||||
|
||||
return CryptoPaymentData(price: price, address: addr, amount: data.amount, paymentUrls: data.paymentUrls);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static String getCurrencyCodeFromPaymentMethod(CakePayPaymentMethod method) {
|
||||
return switch (method) {
|
||||
CakePayPaymentMethod.BTC => 'BTC',
|
||||
CakePayPaymentMethod.BTC_LN => 'BTC',
|
||||
CakePayPaymentMethod.XMR => 'XMR',
|
||||
CakePayPaymentMethod.LTC => 'LTC',
|
||||
CakePayPaymentMethod.LTC_MWEB => 'LTC',
|
||||
CakePayPaymentMethod.ZEC => 'ZEC',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class OrderCard {
|
||||
final int cardId;
|
||||
final int? externalId;
|
||||
final String price;
|
||||
final int quantity;
|
||||
final String currencyCode;
|
||||
final String? cardName;
|
||||
final String? cardImagePath;
|
||||
|
||||
OrderCard({
|
||||
required this.cardId,
|
||||
required this.externalId,
|
||||
required this.price,
|
||||
required this.quantity,
|
||||
required this.currencyCode,
|
||||
required this.cardName,
|
||||
required this.cardImagePath,
|
||||
});
|
||||
|
||||
factory OrderCard.fromMap(Map<String, dynamic> map) {
|
||||
return OrderCard(
|
||||
cardId: map['card_id'] as int,
|
||||
externalId: map['external_id'] as int?,
|
||||
price: map['price'] as String,
|
||||
quantity: map['quantity'] as int,
|
||||
currencyCode: map['currency_code'] as String,
|
||||
cardName: map['name'] as String?,
|
||||
cardImagePath: map['card_image_url'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentData {
|
||||
final CryptoPaymentData? btc;
|
||||
final CryptoPaymentData? btc_ln;
|
||||
final CryptoPaymentData? xmr;
|
||||
final CryptoPaymentData? ltc;
|
||||
final CryptoPaymentData? ltc_mweb;
|
||||
final CryptoPaymentData? zec;
|
||||
final DateTime invoiceTime;
|
||||
final DateTime expirationTime;
|
||||
final int? commission;
|
||||
|
||||
PaymentData({
|
||||
required this.btc,
|
||||
required this.btc_ln,
|
||||
required this.xmr,
|
||||
required this.ltc,
|
||||
required this.ltc_mweb,
|
||||
required this.zec,
|
||||
required this.invoiceTime,
|
||||
required this.expirationTime,
|
||||
required this.commission,
|
||||
});
|
||||
|
||||
factory PaymentData.fromMap(Map<String, dynamic> map) {
|
||||
return PaymentData(
|
||||
btc: map['BTC'] != null ? CryptoPaymentData.fromMap(map['BTC'] as Map<String, dynamic>) : null,
|
||||
btc_ln: map['BTC_LN'] != null ? CryptoPaymentData.fromMap(map['BTC_LN'] as Map<String, dynamic>) : null,
|
||||
xmr: map['XMR'] != null ? CryptoPaymentData.fromMap(map['XMR'] as Map<String, dynamic>) : null,
|
||||
ltc: map['LTC'] != null ? CryptoPaymentData.fromMap(map['LTC'] as Map<String, dynamic>) : null,
|
||||
ltc_mweb: map['LTC_MWEB'] != null ? CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map<String, dynamic>) : null,
|
||||
zec: map['ZEC_CHAIN'] != null ? CryptoPaymentData.fromMap(map['ZEC_CHAIN'] as Map<String, dynamic>) : null,
|
||||
invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int),
|
||||
expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int),
|
||||
commission: map['commission'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoPaymentData {
|
||||
final String price;
|
||||
final String amount;
|
||||
final PaymentUrl? paymentUrls;
|
||||
final String address;
|
||||
|
||||
CryptoPaymentData({
|
||||
required this.price,
|
||||
required this.amount,
|
||||
this.paymentUrls,
|
||||
required this.address,
|
||||
});
|
||||
|
||||
factory CryptoPaymentData.fromMap(Map<String, dynamic> map) {
|
||||
return CryptoPaymentData(
|
||||
price: map['price'] as String,
|
||||
amount: (map['amount_from'] as double?)?.toString() ?? '',
|
||||
paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map<String, dynamic>?),
|
||||
address: map['address'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentUrl {
|
||||
final String? bip21;
|
||||
final String? bip72;
|
||||
final String? bip72b;
|
||||
final String? bip73;
|
||||
final String? bolt11;
|
||||
|
||||
PaymentUrl({
|
||||
this.bip21,
|
||||
this.bip72,
|
||||
this.bip72b,
|
||||
this.bip73,
|
||||
this.bolt11,
|
||||
});
|
||||
|
||||
factory PaymentUrl.fromMap(Map<String, dynamic>? map) {
|
||||
return PaymentUrl(
|
||||
bip21: map?['BIP21'] as String?,
|
||||
bip72: map?['BIP72'] as String?,
|
||||
bip72b: map?['BIP72b'] as String?,
|
||||
bip73: map?['BIP73'] as String?,
|
||||
bolt11: map?['BOLT11'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
class CakePayUserCredentials {
|
||||
const CakePayUserCredentials(this.username, this.token);
|
||||
|
||||
final String username;
|
||||
final String token;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:hash_wallet/entities/country.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
import 'cake_pay_card.dart';
|
||||
|
||||
class CakePayVendor {
|
||||
final int id;
|
||||
final String name;
|
||||
final bool available;
|
||||
final String? cakeWarnings;
|
||||
final String country;
|
||||
final CakePayCard? card;
|
||||
|
||||
CakePayVendor({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.available,
|
||||
this.cakeWarnings,
|
||||
required this.country,
|
||||
this.card,
|
||||
});
|
||||
|
||||
factory CakePayVendor.fromJson(Map<String, dynamic> json) {
|
||||
final name = stripHtmlIfNeeded(json['name'] as String);
|
||||
|
||||
final parsedCountry = json['country'] as String;
|
||||
final country = Country.normalizeName(parsedCountry);
|
||||
|
||||
var cardsJson = json['cards'] as List?;
|
||||
CakePayCard? cardForVendor;
|
||||
|
||||
if (cardsJson != null && cardsJson.isNotEmpty) {
|
||||
try {
|
||||
cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) {
|
||||
return Country.normalizeName(card['country'] as String) == country;
|
||||
}) as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
printV('Error parsing card for vendor: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return CakePayVendor(
|
||||
id: json['id'] as int,
|
||||
name: name,
|
||||
available: json['available'] as bool? ?? false,
|
||||
cakeWarnings: json['cake_warnings'] as String?,
|
||||
country: country,
|
||||
card: cardForVendor,
|
||||
);
|
||||
}
|
||||
|
||||
static String stripHtmlIfNeeded(String text) {
|
||||
return text.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ');
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_user_credentials.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
|
||||
class CakePayApi {
|
||||
static const testBaseUri = FeatureFlag.hasDevOptions;
|
||||
|
||||
static const baseTestCakePayUri = 'api-stg.cakepay.com';
|
||||
static const baseProdCakePayUri = 'api-prod.cakepay.com';
|
||||
|
||||
static const baseCakePayUri = testBaseUri ? baseTestCakePayUri : baseProdCakePayUri;
|
||||
|
||||
static const vendorsPath = '/api/marketplace/vendors';
|
||||
static const authPath = '/api/accounts/auth';
|
||||
static final verifyEmailPath = '/api/accounts/auth/verify';
|
||||
static final logoutPath = '/api/accounts/logout';
|
||||
static final orderPath = '/api/orders/order';
|
||||
static final simulatePaymentPath = '/api/orders/simulate-payment';
|
||||
|
||||
/// AuthenticateUser
|
||||
Future<String> authenticateUser({required String email, required String apiKey}) async {
|
||||
try {
|
||||
final uri = Uri.https(baseCakePayUri, authPath);
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Api-Key $apiKey',
|
||||
};
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode({'email': email}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (bodyJson.containsKey('user') && bodyJson['user']['email'] != null) {
|
||||
return bodyJson['user']['email'] as String;
|
||||
}
|
||||
|
||||
throw Exception('Failed to authenticate user with error: $bodyJson');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to authenticate user with error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify email
|
||||
Future<CakePayUserCredentials> verifyEmail({
|
||||
required String email,
|
||||
required String code,
|
||||
required String apiKey,
|
||||
}) async {
|
||||
final uri = Uri.https(baseCakePayUri, verifyEmailPath);
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Api-Key $apiKey',
|
||||
};
|
||||
final query = <String, String>{'email': email, 'otp': code};
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(query),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (bodyJson.containsKey('error')) {
|
||||
throw Exception(bodyJson['error'] as String);
|
||||
}
|
||||
|
||||
if (bodyJson.containsKey('token')) {
|
||||
final token = bodyJson['token'] as String;
|
||||
final userEmail = bodyJson['user']['email'] as String;
|
||||
return CakePayUserCredentials(userEmail, token);
|
||||
} else {
|
||||
throw Exception('E-mail verification failed.');
|
||||
}
|
||||
}
|
||||
|
||||
/// createOrder
|
||||
Future<CakePayOrder> createOrder({
|
||||
required String apiKey,
|
||||
required int cardId,
|
||||
required String price,
|
||||
required int quantity,
|
||||
required String userEmail,
|
||||
required String token,
|
||||
required bool confirmsNoVpn,
|
||||
required bool confirmsVoidedRefund,
|
||||
required bool confirmsTermsAgreed,
|
||||
}) async {
|
||||
final uri = Uri.https(baseCakePayUri, orderPath);
|
||||
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Token $token',
|
||||
};
|
||||
|
||||
final body = json.encode({
|
||||
'card_id': cardId,
|
||||
'price': price,
|
||||
'quantity': quantity,
|
||||
'user_email': userEmail,
|
||||
'send_email': true,
|
||||
'confirms_no_vpn': confirmsNoVpn,
|
||||
'confirms_voided_refund': confirmsVoidedRefund,
|
||||
'confirms_terms_agreed': confirmsTermsAgreed,
|
||||
});
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
if (response.statusCode == 201) {
|
||||
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||
return CakePayOrder.fromMap(data);
|
||||
}
|
||||
|
||||
String message = 'Server error ${response.statusCode}';
|
||||
|
||||
final isJson = response.headers['content-type']?.contains('application/json') == true ||
|
||||
response.body.trim().startsWith(RegExp(r'[\{\[]'));
|
||||
|
||||
if (isJson) {
|
||||
try {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded is List && decoded.isNotEmpty) {
|
||||
message = decoded.first.toString();
|
||||
} else if (decoded is Map && decoded['detail'] != null) {
|
||||
message = decoded['detail'].toString();
|
||||
} else {
|
||||
message = decoded.toString();
|
||||
}
|
||||
} on FormatException {}
|
||||
}
|
||||
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
/// Get Order by ID
|
||||
Future<CakePayOrder> getOrderById({required String orderId, required String token}) async {
|
||||
final uri = Uri.https(baseCakePayUri, '$orderPath/$orderId');
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Token $token',
|
||||
};
|
||||
final response = await ProxyWrapper().get(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
return CakePayOrder.fromMap(bodyJson);
|
||||
}
|
||||
|
||||
///Simulate Payment
|
||||
Future<String> simulatePayment(
|
||||
{required String CSRFToken,
|
||||
required String authorization,
|
||||
required String orderId,
|
||||
required String token}) async {
|
||||
final uri = Uri.https(baseCakePayUri, simulatePaymentPath);
|
||||
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Token $token',
|
||||
};
|
||||
|
||||
final body = json.encode({
|
||||
'order_id': orderId,
|
||||
});
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: body,
|
||||
);
|
||||
|
||||
printV('Response: ${response.statusCode}');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
return 'You just SIMULATED a buying of a gift card with ID: ${bodyJson['order_id']}';
|
||||
}
|
||||
|
||||
/// Logout
|
||||
Future<void> logoutUser({required String email, required String apiKey}) async {
|
||||
final uri = Uri.https(baseCakePayUri, logoutPath);
|
||||
final headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Api-Key $apiKey',
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode({'email': email}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
printV('Caught exception: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Vendors
|
||||
Future<List<CakePayVendor>> getVendors({
|
||||
required String apiKey,
|
||||
required int page,
|
||||
required String countryCode,
|
||||
String? country,
|
||||
String? search,
|
||||
List<String>? vendorIds,
|
||||
bool? giftCards = true,
|
||||
bool? prepaidCards = true,
|
||||
bool? onDemand,
|
||||
bool? custom,
|
||||
}) async {
|
||||
var queryParams = {
|
||||
'page': page.toString(),
|
||||
'country_code': countryCode,
|
||||
if (search != null && search.isNotEmpty) 'search': search,
|
||||
if (vendorIds != null && vendorIds.isNotEmpty) 'vendor_ids': vendorIds.join(','),
|
||||
};
|
||||
|
||||
if (giftCards == false || prepaidCards == false) {
|
||||
queryParams['gift_cards'] = giftCards.toString();
|
||||
queryParams['prepaid_cards'] = prepaidCards.toString();
|
||||
}
|
||||
|
||||
final uri = Uri.https(baseCakePayUri, vendorsPath, queryParams);
|
||||
|
||||
var headers = {
|
||||
'accept': 'application/json; charset=UTF-8',
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Authorization': 'Api-Key $apiKey',
|
||||
};
|
||||
|
||||
var response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to fetch vendors: statusCode - ${response.statusCode}, queryParams -$queryParams, response - ${response.body}');
|
||||
}
|
||||
|
||||
final bodyJson = json.decode(response.body);
|
||||
|
||||
if (bodyJson is List<dynamic> && bodyJson.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (bodyJson['results'] as List)
|
||||
.map((e) => CakePayVendor.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_api.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:hash_wallet/core/secure_storage.dart';
|
||||
|
||||
class CakePayService {
|
||||
CakePayService(this.secureStorage, this.cakePayApi);
|
||||
|
||||
/// `_v2` was added to log out users so they can login using the new backend
|
||||
static const cakePayEmailStorageKey = 'cake_pay_email_v2';
|
||||
static const cakePayUsernameStorageKey = 'cake_pay_username_v2';
|
||||
static const cakePayUserTokenKey = 'cake_pay_user_token_v2';
|
||||
|
||||
static String get testCakePayApiKey => secrets.testCakePayApiKey;
|
||||
|
||||
static String get cakePayApiKey => secrets.cakePayApiKey;
|
||||
|
||||
static String get CSRFToken => secrets.CSRFToken;
|
||||
|
||||
static String get authorization => secrets.authorization;
|
||||
|
||||
final SecureStorage secureStorage;
|
||||
final CakePayApi cakePayApi;
|
||||
|
||||
/// Get Vendors
|
||||
Future<List<CakePayVendor>> getVendors({
|
||||
required int page,
|
||||
required String countryCode,
|
||||
String? country,
|
||||
String? search,
|
||||
List<String>? vendorIds,
|
||||
bool? giftCards,
|
||||
bool? prepaidCards,
|
||||
bool? onDemand,
|
||||
bool? custom,
|
||||
}) async {
|
||||
final result = await cakePayApi.getVendors(
|
||||
apiKey: cakePayApiKey,
|
||||
page: page,
|
||||
country: country,
|
||||
countryCode: countryCode,
|
||||
search: search,
|
||||
vendorIds: vendorIds,
|
||||
giftCards: giftCards,
|
||||
prepaidCards: prepaidCards,
|
||||
onDemand: onDemand,
|
||||
custom: custom);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// LogIn
|
||||
Future<void> logIn(String email) async {
|
||||
final userName = await cakePayApi.authenticateUser(email: email, apiKey: cakePayApiKey);
|
||||
await secureStorage.write(key: cakePayEmailStorageKey, value: userName);
|
||||
await secureStorage.write(key: cakePayUsernameStorageKey, value: userName);
|
||||
}
|
||||
|
||||
/// Verify email
|
||||
Future<void> verifyEmail(String code) async {
|
||||
final email = (await secureStorage.read(key: cakePayEmailStorageKey))!;
|
||||
final credentials =
|
||||
await cakePayApi.verifyEmail(email: email, code: code, apiKey: cakePayApiKey);
|
||||
await secureStorage.write(key: cakePayUserTokenKey, value: credentials.token);
|
||||
await secureStorage.write(key: cakePayUsernameStorageKey, value: credentials.username);
|
||||
}
|
||||
|
||||
Future<String?> getUserEmail() async {
|
||||
return (await secureStorage.read(key: cakePayEmailStorageKey));
|
||||
}
|
||||
|
||||
/// Check is user logged
|
||||
Future<bool> isLogged() async {
|
||||
final username = await secureStorage.read(key: cakePayUsernameStorageKey) ?? '';
|
||||
final password = await secureStorage.read(key: cakePayUserTokenKey) ?? '';
|
||||
return username.isNotEmpty && password.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Logout
|
||||
Future<void> logout([String? email]) async {
|
||||
await secureStorage.delete(key: cakePayUsernameStorageKey);
|
||||
await secureStorage.delete(key: cakePayUserTokenKey);
|
||||
if (email != null) {
|
||||
await cakePayApi.logoutUser(email: email, apiKey: cakePayApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
/// Purchase Gift Card
|
||||
Future<CakePayOrder> createOrder({
|
||||
required int cardId,
|
||||
required String price,
|
||||
required int quantity,
|
||||
required bool confirmsNoVpn,
|
||||
required bool confirmsVoidedRefund,
|
||||
required bool confirmsTermsAgreed,
|
||||
}) async {
|
||||
final userEmail = (await secureStorage.read(key: cakePayEmailStorageKey))!;
|
||||
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
|
||||
return await cakePayApi.createOrder(
|
||||
apiKey: cakePayApiKey,
|
||||
cardId: cardId,
|
||||
price: price,
|
||||
quantity: quantity,
|
||||
token: token,
|
||||
userEmail: userEmail,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
confirmsTermsAgreed: confirmsTermsAgreed,
|
||||
);
|
||||
}
|
||||
|
||||
Future<CakePayOrder> findOrderById({required String orderId}) async {
|
||||
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
|
||||
return await cakePayApi.getOrderById(orderId: orderId, token: token);
|
||||
}
|
||||
|
||||
///Simulate Purchase Gift Card
|
||||
Future<String> simulatePayment({required String orderId}) async {
|
||||
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
|
||||
return await cakePayApi.simulatePayment(
|
||||
CSRFToken: CSRFToken, authorization: authorization, token: token, orderId: orderId);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/alert_background.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CakePayAlertModal extends StatelessWidget {
|
||||
const CakePayAlertModal({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.actionTitle,
|
||||
this.showCloseButton = true,
|
||||
this.dismissible = false,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final Widget content;
|
||||
final String actionTitle;
|
||||
final bool showCloseButton;
|
||||
final bool dismissible;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.8;
|
||||
|
||||
return AlertBackground(
|
||||
dismissible: dismissible,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(30)),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (title.isNotEmpty) ...[
|
||||
Text(title,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Flexible(child: SingleChildScrollView(child: content)),
|
||||
const SizedBox(height: 24),
|
||||
PrimaryButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
text: actionTitle,
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
textColor: theme.colorScheme.primary),
|
||||
if (showCloseButton) ...[
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: theme.colorScheme.surfaceContainer,
|
||||
child: Icon(Icons.close, color: theme.colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import 'package:hash_wallet/entities/country.dart';
|
||||
import 'package:hash_wallet/src/widgets/search_bar_widget.dart';
|
||||
import 'package:hash_wallet/utils/debounce.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CakePaySearchBar extends StatefulWidget {
|
||||
const CakePaySearchBar(
|
||||
{required this.initialQuery,
|
||||
required this.onSearch,
|
||||
required this.onFilter,
|
||||
this.onCountryPick,
|
||||
this.controller,
|
||||
this.selectedCountry});
|
||||
|
||||
final String initialQuery;
|
||||
final ValueChanged<String> onSearch;
|
||||
final VoidCallback onFilter;
|
||||
final VoidCallback? onCountryPick;
|
||||
final Country? selectedCountry;
|
||||
final TextEditingController? controller;
|
||||
|
||||
@override
|
||||
State<CakePaySearchBar> createState() => _CakePaySearchBarState();
|
||||
}
|
||||
|
||||
class _CakePaySearchBarState extends State<CakePaySearchBar> {
|
||||
late final TextEditingController _searchController =
|
||||
widget.controller ?? TextEditingController(text: widget.initialQuery);
|
||||
final _searchFocusNode = FocusNode();
|
||||
final _debounce = Debounce(const Duration(milliseconds: 500));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController
|
||||
.addListener(() => _debounce.run(() => widget.onSearch(_searchController.text)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: SearchBarWidget(searchController: _searchController)),
|
||||
const SizedBox(width: 5),
|
||||
_CakePayFilterButton(onFilter: widget.onFilter),
|
||||
if (widget.selectedCountry != null)
|
||||
_CountryPickerWidget(
|
||||
onTap: widget.onCountryPick,
|
||||
selectedCountry: widget.selectedCountry!,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CakePayFilterButton extends StatelessWidget {
|
||||
const _CakePayFilterButton({required this.onFilter});
|
||||
|
||||
final VoidCallback onFilter;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onFilter,
|
||||
child: Container(
|
||||
width: 32,
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Image.asset('assets/images/filter_icon.png',
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CountryPickerWidget extends StatelessWidget {
|
||||
const _CountryPickerWidget({required this.selectedCountry, this.onTap});
|
||||
|
||||
final Country selectedCountry;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(selectedCountry.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
errorBuilder: (_, __, ___) => const SizedBox(width: 24, height: 24)),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
selectedCountry.countryCode,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CakePayTile extends StatelessWidget {
|
||||
const CakePayTile({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.subTitle,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final String title;
|
||||
final String subTitle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
subTitle,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'image_placeholder.dart';
|
||||
|
||||
class CardItem extends StatelessWidget {
|
||||
CardItem({
|
||||
required this.title,
|
||||
required this.subTitle,
|
||||
this.isAmount = false,
|
||||
this.onTap,
|
||||
this.logoUrl,
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final String title;
|
||||
final String subTitle;
|
||||
final String? logoUrl;
|
||||
final bool isAmount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (logoUrl != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 1.8,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
child: Image.network(
|
||||
logoUrl!,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder:
|
||||
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.w700)),
|
||||
Text(subTitle,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(fontSize: 10, fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/number_text_fild_widget.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dropdown_filter_item_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class DenominationsAmountWidget extends StatefulWidget {
|
||||
const DenominationsAmountWidget({
|
||||
required this.fiatCurrency,
|
||||
required this.denominations,
|
||||
required this.amountFieldFocus,
|
||||
required this.amountController,
|
||||
required this.quantityFieldFocus,
|
||||
required this.quantityController,
|
||||
required this.cakePayBuyCardViewModel,
|
||||
required this.onAmountChanged,
|
||||
required this.onQuantityChanged,
|
||||
});
|
||||
|
||||
final String fiatCurrency;
|
||||
final List<Denomination> denominations;
|
||||
final FocusNode amountFieldFocus;
|
||||
final TextEditingController amountController;
|
||||
final FocusNode quantityFieldFocus;
|
||||
final TextEditingController quantityController;
|
||||
final CakePayBuyCardViewModel cakePayBuyCardViewModel;
|
||||
final Function(String) onAmountChanged;
|
||||
final Function(int?) onQuantityChanged;
|
||||
|
||||
@override
|
||||
State<DenominationsAmountWidget> createState() => _DenominationsAmountWidgetState();
|
||||
}
|
||||
|
||||
class _DenominationsAmountWidgetState extends State<DenominationsAmountWidget> {
|
||||
late (String, int?) _selected;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final first = widget.denominations.first;
|
||||
final amount = widget.amountController.text.isNotEmpty
|
||||
? widget.amountController.text
|
||||
: first.value.toString();
|
||||
_selected = (amount, first.cardId);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = _selected;
|
||||
|
||||
widget.amountController.text = _selected.$1;
|
||||
widget.onAmountChanged(_selected.$1);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.1,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
DropdownFilterList(
|
||||
items: widget.denominations
|
||||
.map((e) => e.value.toString())
|
||||
.toList(),
|
||||
itemPrefix: widget.fiatCurrency,
|
||||
selectedItem: _selected.$1,
|
||||
onItemSelected: (value) {
|
||||
setState(() => _selected = (value, widget.denominations
|
||||
.firstWhere((e) => e.value.toString() == value)
|
||||
.cardId));
|
||||
widget.amountController.text = value;
|
||||
widget.onAmountChanged(value);
|
||||
widget.cakePayBuyCardViewModel.selectedDenomination = (_selected.$1, _selected.$2);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).value,
|
||||
maxLines: 2,
|
||||
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
NumberTextField(
|
||||
controller: widget.quantityController,
|
||||
focusNode: widget.quantityFieldFocus,
|
||||
min: 1,
|
||||
max: 99,
|
||||
onChanged: (value) => widget.onQuantityChanged(value),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).quantity,
|
||||
maxLines: 1,
|
||||
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => Text(
|
||||
'${widget.fiatCurrency} ${widget.cakePayBuyCardViewModel.totalAmount}',
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).total,
|
||||
maxLines: 1,
|
||||
style: textSmall(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class EnterAmountWidget extends StatelessWidget {
|
||||
const EnterAmountWidget(
|
||||
{required this.minValue,
|
||||
required this.maxValue,
|
||||
required this.fiatCurrency,
|
||||
required this.amountFieldFocus,
|
||||
required this.amountController,
|
||||
required this.onAmountChanged});
|
||||
|
||||
final String minValue;
|
||||
final String maxValue;
|
||||
final String fiatCurrency;
|
||||
final FocusNode amountFieldFocus;
|
||||
final TextEditingController amountController;
|
||||
final Function(String) onAmountChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.1,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Spacer(flex: 1),
|
||||
Text(
|
||||
S.of(context).enter_amount,
|
||||
style: Theme.of(context).textTheme.titleLarge!,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
width: 1.0, color: Theme.of(context).colorScheme.onSurfaceVariant))),
|
||||
child: BaseTextFormField(
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
hintText: '0.00',
|
||||
maxLines: 1,
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Text(
|
||||
'$fiatCurrency: ',
|
||||
style: Theme.of(context).textTheme.titleMedium!,
|
||||
),
|
||||
),
|
||||
prefixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
|
||||
suffixIconConstraints: BoxConstraints(minWidth: 0, minHeight: 0),
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.only(top: 2, right: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('QTY ', style: Theme.of(context).textTheme.titleMedium!),
|
||||
Text(
|
||||
'1',
|
||||
style: Theme.of(context).textTheme
|
||||
.titleMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.titleMedium!,
|
||||
placeholderTextStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.deny(RegExp('[\-|\ ]')),
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'^\d+(\.|\,)?\d{0,2}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).min_amount(minValue) + ' $fiatCurrency',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
Text(S.of(context).max_amount(maxValue) + ' $fiatCurrency',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
)),
|
||||
],
|
||||
),
|
||||
Spacer(flex: 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlipCard extends StatefulWidget {
|
||||
const FlipCard({
|
||||
super.key,
|
||||
required this.front,
|
||||
required this.back,
|
||||
this.duration = const Duration(milliseconds: 400),
|
||||
this.flipOnTouch = true,
|
||||
});
|
||||
|
||||
final Widget front;
|
||||
final Widget back;
|
||||
final Duration duration;
|
||||
final bool flipOnTouch;
|
||||
|
||||
@override
|
||||
FlipCardState createState() => FlipCardState();
|
||||
}
|
||||
|
||||
class FlipCardState extends State<FlipCard> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _ctrl =
|
||||
AnimationController(vsync: this, duration: widget.duration);
|
||||
bool _isFront = true;
|
||||
|
||||
void toggleCard() {
|
||||
if (_isFront) {
|
||||
_ctrl.forward();
|
||||
} else {
|
||||
_ctrl.reverse();
|
||||
}
|
||||
_isFront = !_isFront;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = AnimatedBuilder(
|
||||
animation: _ctrl,
|
||||
builder: (_, __) {
|
||||
final angle = _ctrl.value * math.pi;
|
||||
final isFront = angle < math.pi / 2;
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateY(angle),
|
||||
child: isFront ? widget.front
|
||||
: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child: widget.back,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return widget.flipOnTouch
|
||||
? GestureDetector(onTap: toggleCard, child: content)
|
||||
: content;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CakePayCardImagePlaceholder extends StatelessWidget {
|
||||
const CakePayCardImagePlaceholder({this.text});
|
||||
|
||||
final String? text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.8,
|
||||
child: Container(
|
||||
child: Center(
|
||||
child: Text(
|
||||
text ?? 'Image not found!',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ClickableLinksText extends StatelessWidget {
|
||||
const ClickableLinksText({
|
||||
required this.text,
|
||||
required this.textStyle,
|
||||
this.linkStyle,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final TextStyle textStyle;
|
||||
final TextStyle? linkStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<InlineSpan> spans = [];
|
||||
RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
|
||||
Iterable<Match> matches = linkRegExp.allMatches(text);
|
||||
|
||||
int previousEnd = 0;
|
||||
matches.forEach((match) {
|
||||
if (match.start > previousEnd) {
|
||||
spans.add(TextSpan(text: text.substring(previousEnd, match.start), style: textStyle));
|
||||
}
|
||||
String url = text.substring(match.start, match.end);
|
||||
if (url.toLowerCase().endsWith('.md')) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: url,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: 18,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: url,
|
||||
style: linkStyle,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(Uri.parse(url));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
previousEnd = match.end;
|
||||
});
|
||||
|
||||
if (previousEnd < text.length) {
|
||||
spans.add(TextSpan(text: text.substring(previousEnd), style: textStyle));
|
||||
}
|
||||
|
||||
return RichText(text: TextSpan(children: spans));
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RoundedOverlayCards extends StatelessWidget {
|
||||
const RoundedOverlayCards({
|
||||
this.topCardChild = const SizedBox(),
|
||||
this.bottomCardChild = const SizedBox(),
|
||||
});
|
||||
|
||||
final Widget topCardChild;
|
||||
final Widget bottomCardChild;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
return ClipRRect(
|
||||
borderRadius:
|
||||
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
child: Container(
|
||||
height: screenHeight * 0.50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
height: screenHeight * 0.38,
|
||||
width: double.infinity,
|
||||
child: topCardChild)),
|
||||
bottomCardChild,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextIconButton extends StatelessWidget {
|
||||
const TextIconButton({
|
||||
Key? key,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_alert_dialog.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class ThreeCheckboxAlert extends BaseAlertDialog {
|
||||
ThreeCheckboxAlert({
|
||||
required this.alertTitle,
|
||||
required this.leftButtonText,
|
||||
required this.rightButtonText,
|
||||
required this.actionLeftButton,
|
||||
required this.actionRightButton,
|
||||
this.alertBarrierDismissible = true,
|
||||
Key? key,
|
||||
});
|
||||
|
||||
final String alertTitle;
|
||||
final String leftButtonText;
|
||||
final String rightButtonText;
|
||||
final VoidCallback actionLeftButton;
|
||||
final Function(bool, bool, bool) actionRightButton;
|
||||
final bool alertBarrierDismissible;
|
||||
|
||||
bool checkbox1 = false;
|
||||
void toggleCheckbox1() => checkbox1 = !checkbox1;
|
||||
bool checkbox2 = false;
|
||||
void toggleCheckbox2() => checkbox2 = !checkbox2;
|
||||
bool checkbox3 = false;
|
||||
void toggleCheckbox3() => checkbox3 = !checkbox3;
|
||||
|
||||
bool showValidationMessage = true;
|
||||
|
||||
@override
|
||||
String get titleText => alertTitle;
|
||||
|
||||
@override
|
||||
bool get isDividerExists => true;
|
||||
|
||||
@override
|
||||
String get leftActionButtonText => leftButtonText;
|
||||
|
||||
@override
|
||||
String get rightActionButtonText => rightButtonText;
|
||||
|
||||
@override
|
||||
VoidCallback get actionLeft => actionLeftButton;
|
||||
|
||||
@override
|
||||
VoidCallback get actionRight => () {
|
||||
actionRightButton(checkbox1, checkbox2, checkbox3);
|
||||
};
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => alertBarrierDismissible;
|
||||
|
||||
@override
|
||||
Widget content(BuildContext context) {
|
||||
return ThreeCheckboxAlertContent(
|
||||
checkbox1: checkbox1,
|
||||
toggleCheckbox1: toggleCheckbox1,
|
||||
checkbox2: checkbox2,
|
||||
toggleCheckbox2: toggleCheckbox2,
|
||||
checkbox3: checkbox3,
|
||||
toggleCheckbox3: toggleCheckbox3,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThreeCheckboxAlertContent extends StatefulWidget {
|
||||
ThreeCheckboxAlertContent({
|
||||
required this.checkbox1,
|
||||
required this.toggleCheckbox1,
|
||||
required this.checkbox2,
|
||||
required this.toggleCheckbox2,
|
||||
required this.checkbox3,
|
||||
required this.toggleCheckbox3,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
bool checkbox1;
|
||||
void Function() toggleCheckbox1;
|
||||
bool checkbox2;
|
||||
void Function() toggleCheckbox2;
|
||||
bool checkbox3;
|
||||
void Function() toggleCheckbox3;
|
||||
|
||||
@override
|
||||
_ThreeCheckboxAlertContentState createState() => _ThreeCheckboxAlertContentState(
|
||||
checkbox1: checkbox1,
|
||||
toggleCheckbox1: toggleCheckbox1,
|
||||
checkbox2: checkbox2,
|
||||
toggleCheckbox2: toggleCheckbox2,
|
||||
checkbox3: checkbox3,
|
||||
toggleCheckbox3: toggleCheckbox3,
|
||||
);
|
||||
|
||||
static _ThreeCheckboxAlertContentState? of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<_ThreeCheckboxAlertContentState>();
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreeCheckboxAlertContentState extends State<ThreeCheckboxAlertContent> {
|
||||
_ThreeCheckboxAlertContentState({
|
||||
required this.checkbox1,
|
||||
required this.toggleCheckbox1,
|
||||
required this.checkbox2,
|
||||
required this.toggleCheckbox2,
|
||||
required this.checkbox3,
|
||||
required this.toggleCheckbox3,
|
||||
});
|
||||
|
||||
bool checkbox1;
|
||||
void Function() toggleCheckbox1;
|
||||
bool checkbox2;
|
||||
void Function() toggleCheckbox2;
|
||||
bool checkbox3;
|
||||
void Function() toggleCheckbox3;
|
||||
|
||||
bool showValidationMessage = true;
|
||||
|
||||
bool get areAllCheckboxesChecked => checkbox1 && checkbox2 && checkbox3;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StandardCheckbox(
|
||||
value: checkbox1,
|
||||
caption: S.of(context).cakepay_confirm_no_vpn,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkbox1 = value ?? false;
|
||||
toggleCheckbox1();
|
||||
showValidationMessage = !areAllCheckboxesChecked;
|
||||
});
|
||||
},
|
||||
),
|
||||
StandardCheckbox(
|
||||
value: checkbox2,
|
||||
caption: S.of(context).cakepay_confirm_voided_refund,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkbox2 = value ?? false;
|
||||
toggleCheckbox2();
|
||||
showValidationMessage = !areAllCheckboxesChecked;
|
||||
});
|
||||
},
|
||||
),
|
||||
StandardCheckbox(
|
||||
value: checkbox3,
|
||||
caption: S.of(context).cakepay_confirm_terms_agreed,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkbox3 = value ?? false;
|
||||
toggleCheckbox3();
|
||||
showValidationMessage = !areAllCheckboxesChecked;
|
||||
});
|
||||
},
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse("https://cakepay.com/cakepay-web-terms.txt"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
S.of(context).settings_terms_and_conditions,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.blueAccent,
|
||||
decoration: TextDecoration.none,
|
||||
height: 1,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showValidationMessage)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Please confirm all checkboxes',
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'image_placeholder.dart';
|
||||
|
||||
class UserCardItem extends StatelessWidget {
|
||||
UserCardItem({
|
||||
required this.title,
|
||||
required this.subTitle,
|
||||
this.onTap,
|
||||
this.logoUrl
|
||||
});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final String title;
|
||||
final String subTitle;
|
||||
final String? logoUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
if (logoUrl != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.65,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(13)),
|
||||
child: Image.network(
|
||||
logoUrl!,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder:
|
||||
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) => CakePayCardImagePlaceholder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(subTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontSize: 16, fontWeight: FontWeight.w700)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:hash_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/store/app_store.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -38,31 +27,13 @@ class TradeMonitor {
|
||||
final SharedPreferences preferences;
|
||||
|
||||
ExchangeProvider? _getProviderByDescription(ExchangeProviderDescription description) {
|
||||
// Providers whose implementations were removed (ChangeNow, SideShift,
|
||||
// SimpleSwap, Exolix, ThorChain, SwapTrade, LetsExchange, StealthEx,
|
||||
// Chainflip, XOSwap, SwapsXyz) fall through to null; their trades are
|
||||
// skipped as "provider not supported".
|
||||
switch (description) {
|
||||
case ExchangeProviderDescription.changeNow:
|
||||
return ChangeNowExchangeProvider(settingsStore: appStore.settingsStore);
|
||||
case ExchangeProviderDescription.sideShift:
|
||||
return SideShiftExchangeProvider();
|
||||
case ExchangeProviderDescription.simpleSwap:
|
||||
return SimpleSwapExchangeProvider();
|
||||
case ExchangeProviderDescription.trocador:
|
||||
return TrocadorExchangeProvider();
|
||||
case ExchangeProviderDescription.exolix:
|
||||
return ExolixExchangeProvider();
|
||||
case ExchangeProviderDescription.thorChain:
|
||||
return ThorChainExchangeProvider();
|
||||
case ExchangeProviderDescription.swapTrade:
|
||||
return SwapTradeExchangeProvider();
|
||||
case ExchangeProviderDescription.letsExchange:
|
||||
return LetsExchangeExchangeProvider();
|
||||
case ExchangeProviderDescription.stealthEx:
|
||||
return StealthExExchangeProvider();
|
||||
case ExchangeProviderDescription.chainflip:
|
||||
return ChainflipExchangeProvider();
|
||||
case ExchangeProviderDescription.xoSwap:
|
||||
return XOSwapExchangeProvider();
|
||||
case ExchangeProviderDescription.swapsXyz:
|
||||
return SwapsXyzExchangeProvider();
|
||||
case ExchangeProviderDescription.nearIntents:
|
||||
return NearIntentsExchangeProvider();
|
||||
}
|
||||
|
||||
+47
-94
@@ -8,14 +8,7 @@ import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/bitcoin_cash/bitcoin_cash.dart';
|
||||
import 'package:hash_wallet/entities/bitcoin_amount_display_mode.dart';
|
||||
import 'package:hash_wallet/evm/evm.dart';
|
||||
import 'package:hash_wallet/buy/dfx/dfx_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart';
|
||||
import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/robinhood/robinhood_buy_provider.dart';
|
||||
import 'package:hash_wallet/cake_pay/cake_pay.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_api.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/core/backup_service_v3.dart';
|
||||
import 'package:hash_wallet/core/key_service.dart';
|
||||
@@ -209,14 +202,9 @@ import 'package:hash_wallet/view_model/bridge/bridge_history_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/buy/buy_amount_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/buy/buy_sell_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/buy/buy_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_account_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_auth_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_cards_list_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/contact_list/contact_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/balance_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/home_settings_view_model.dart';
|
||||
@@ -309,8 +297,6 @@ import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:trezor_connect/trezor_connect.dart';
|
||||
|
||||
import 'buy/kryptonim/kryptonim.dart';
|
||||
import 'buy/meld/meld_buy_provider.dart';
|
||||
import 'dogecoin/dogecoin.dart';
|
||||
import 'new-ui/viewmodels/card_customizer/card_customizer_bloc.dart';
|
||||
import 'new-ui/widgets/addresses_page/address_info.dart';
|
||||
@@ -376,6 +362,34 @@ Future<void> setup({
|
||||
return;
|
||||
}
|
||||
|
||||
_registerCoreSingletons(settingsStore);
|
||||
|
||||
final walletList = await WalletInfo.getAll();
|
||||
|
||||
_registerCoreViewModels(settingsStore);
|
||||
|
||||
_registerAuthAndSecurity(walletList, navigatorKey);
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
|
||||
_registerDashboardAndSendPages(appStore);
|
||||
|
||||
_registerWalletListAndAccounts();
|
||||
|
||||
_registerSettingsAndNodes(walletList);
|
||||
|
||||
_registerExchangeAndSwap();
|
||||
|
||||
_registerRestoreAndBackup();
|
||||
|
||||
_registerBuyAndSupport();
|
||||
|
||||
_registerDevToolsAndBridge(appStore);
|
||||
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
||||
void _registerCoreSingletons(SettingsStore settingsStore) {
|
||||
getIt.registerFactory<Box<Node>>(() => _nodeSource);
|
||||
getIt.registerFactory<Box<Node>>(() => _powNodeSource, instanceName: Node.boxName + "pow");
|
||||
|
||||
@@ -457,10 +471,9 @@ Future<void> setup({
|
||||
getIt.get<SeedSettingsViewModel>(),
|
||||
newWalletArguments: newWalletArgs,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
final walletList = await WalletInfo.getAll();
|
||||
|
||||
void _registerCoreViewModels(SettingsStore settingsStore) {
|
||||
getIt.registerFactory<WalletManager>(
|
||||
() => WalletManager(
|
||||
getIt.get<SharedPreferences>(),
|
||||
@@ -605,7 +618,9 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory<LightningUsernameBloc>(
|
||||
() => LightningUsernameBloc(getIt.get<AppStore>().wallet!));
|
||||
}
|
||||
|
||||
void _registerAuthAndSecurity(List<WalletInfo> walletList, GlobalKey<NavigatorState> navigatorKey) {
|
||||
getIt.registerFactory<AuthService>(
|
||||
() => AuthService(
|
||||
secureStorage: getIt.get<SecureStorage>(),
|
||||
@@ -751,9 +766,9 @@ Future<void> setup({
|
||||
});
|
||||
|
||||
getIt.registerSingleton<BottomSheetService>(BottomSheetServiceImpl());
|
||||
}
|
||||
|
||||
final appStore = getIt.get<AppStore>();
|
||||
|
||||
void _registerDashboardAndSendPages(AppStore appStore) {
|
||||
getIt.registerLazySingleton<WalletConnectKeyService>(() => KeyServiceImpl());
|
||||
|
||||
getIt.registerLazySingleton<WalletKitService>(() {
|
||||
@@ -934,7 +949,9 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(
|
||||
() => SendTemplatePage(sendTemplateViewModel: getIt.get<SendTemplateViewModel>()));
|
||||
}
|
||||
|
||||
void _registerWalletListAndAccounts() {
|
||||
if (DeviceInfo.instance.isMobile) {
|
||||
getIt.registerFactory(
|
||||
() => WalletListViewModel(
|
||||
@@ -1050,7 +1067,9 @@ Future<void> setup({
|
||||
(NanoAccount? account, _) => NanoAccountEditOrCreatePage(
|
||||
nanoAccountCreationViewModel:
|
||||
getIt.get<NanoAccountEditOrCreateViewModel>(param1: account)));
|
||||
}
|
||||
|
||||
void _registerSettingsAndNodes(List<WalletInfo> walletList) {
|
||||
getIt.registerFactory(() =>
|
||||
DisplaySettingsViewModel(getIt.get<AppStore>(), getIt.get<ThemeStore>()));
|
||||
|
||||
@@ -1198,39 +1217,15 @@ Future<void> setup({
|
||||
nodeCreateOrEditViewModel: getIt.get<NodeCreateOrEditViewModel>(param1: {'isPow' : true, 'editingNode': editingNode}),
|
||||
editingNode: editingNode,
|
||||
isSelected: isSelected));
|
||||
}
|
||||
|
||||
getIt.registerFactory<RobinhoodBuyProvider>(() => RobinhoodBuyProvider(
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
hardwareWalletVM: getIt<AppStore>().wallet!.isHardwareWallet
|
||||
? getIt<HardwareWalletViewModel>(param1: getIt<AppStore>().wallet!.hardwareWalletType!)
|
||||
: null,
|
||||
));
|
||||
|
||||
getIt.registerFactory<DFXBuyProvider>(() => DFXBuyProvider(
|
||||
wallet: getIt<AppStore>().wallet!,
|
||||
hardwareWalletVM: getIt<AppStore>().wallet!.isHardwareWallet
|
||||
? getIt<HardwareWalletViewModel>(param1: getIt<AppStore>().wallet!.hardwareWalletType!)
|
||||
: null,
|
||||
));
|
||||
|
||||
void _registerExchangeAndSwap() {
|
||||
getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
|
||||
appStore: getIt.get<AppStore>(),
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
isTestEnvironment: kDebugMode || kProfileMode,
|
||||
));
|
||||
|
||||
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
));
|
||||
|
||||
getIt.registerFactory<MeldBuyProvider>(() => MeldBuyProvider(
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
));
|
||||
|
||||
getIt.registerFactory<KryptonimBuyProvider>(() => KryptonimBuyProvider(
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
));
|
||||
|
||||
getIt.registerFactoryParam<WebViewPage, String, Uri>((title, uri) => WebViewPage(title, uri));
|
||||
|
||||
getIt.registerFactory<FeesViewModel>(
|
||||
@@ -1335,7 +1330,9 @@ Future<void> setup({
|
||||
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _registerRestoreAndBackup() {
|
||||
getIt.registerFactoryParam<SetupPinCodeViewModel, bool?, void>(
|
||||
(isDuressPin, _) => SetupPinCodeViewModel(
|
||||
getIt.get<AuthService>(),
|
||||
@@ -1446,12 +1443,8 @@ Future<void> setup({
|
||||
getIt.registerFactoryParam<TradeDetailsViewModel, Trade, void>(
|
||||
(trade, _) => TradeDetailsViewModel(tradeForDetails: trade, appStore: getIt.get<AppStore>()));
|
||||
|
||||
getIt.registerFactory(() => CakeFeaturesViewModel(getIt.get<CakePayService>()));
|
||||
|
||||
|
||||
getIt.registerFactory(() => CakeFeaturesPage(
|
||||
dashboardViewModel: getIt.get<DashboardViewModel>(),
|
||||
cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>()));
|
||||
dashboardViewModel: getIt.get<DashboardViewModel>()));
|
||||
|
||||
getIt.registerFactory(() => BackupServiceV3(getIt.get<SecureStorage>(),
|
||||
_transactionDescriptionBox,
|
||||
@@ -1478,7 +1471,9 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactoryParam<TradeDetailsPage, Trade, void>(
|
||||
(Trade trade, _) => TradeDetailsPage(getIt.get<TradeDetailsViewModel>(param1: trade)));
|
||||
}
|
||||
|
||||
void _registerBuyAndSupport() {
|
||||
getIt.registerFactory(() => BuyAmountViewModel());
|
||||
|
||||
getIt.registerFactory(() => BuySellViewModel(getIt.get<AppStore>()));
|
||||
@@ -1519,12 +1514,10 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
|
||||
final wallet = getIt.get<AppStore>().wallet;
|
||||
final cakePayService = getIt.get<CakePayService>();
|
||||
|
||||
return OrderDetailsViewModel(
|
||||
wallet: wallet!,
|
||||
orderForDetails: order,
|
||||
cakePayService: cakePayService,
|
||||
orders: _ordersSource);
|
||||
});
|
||||
|
||||
@@ -1578,7 +1571,9 @@ Future<void> setup({
|
||||
unspentCoinsDetailsViewModel: getIt.get<UnspentCoinsDetailsViewModel>(
|
||||
param1: item, param2: unspentCoinsListViewModel));
|
||||
});
|
||||
}
|
||||
|
||||
void _registerDevToolsAndBridge(AppStore appStore) {
|
||||
getIt.registerFactory(() => AddressResolver(
|
||||
wallet: getIt.get<AppStore>().wallet!,
|
||||
settingsStore: getIt.get<SettingsStore>()));
|
||||
@@ -1586,48 +1581,8 @@ Future<void> setup({
|
||||
getIt.registerFactoryParam<FullscreenQRPage, QrViewData, void>(
|
||||
(QrViewData viewData, _) => FullscreenQRPage(qrViewData: viewData));
|
||||
|
||||
getIt.registerFactory(() => CakePayApi());
|
||||
|
||||
getIt.registerFactory(() => AnyPayApi());
|
||||
|
||||
getIt.registerFactory<CakePayService>(
|
||||
() => CakePayService(getIt.get<SecureStorage>(), getIt.get<CakePayApi>()));
|
||||
|
||||
getIt.registerFactory(
|
||||
() => CakePayCardsListViewModel(cakePayService: getIt.get<CakePayService>(),
|
||||
settingsStore: getIt.get<SettingsStore>()));
|
||||
|
||||
getIt.registerFactory(() => CakePayAuthViewModel(cakePayService: getIt.get<CakePayService>()));
|
||||
|
||||
getIt.registerFactoryParam<CakePayBuyCardViewModel, CakePayVendor, void>(
|
||||
(CakePayVendor vendor, _) {
|
||||
return CakePayBuyCardViewModel(vendor: vendor,
|
||||
cakePayService: getIt.get<CakePayService>(),
|
||||
sendViewModel: getIt.get<SendViewModel>(),
|
||||
orders: _ordersSource);
|
||||
});
|
||||
|
||||
getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get<CakePayService>()));
|
||||
|
||||
getIt.registerFactory(() => CakePayWelcomePage(getIt.get<CakePayAuthViewModel>()));
|
||||
|
||||
getIt.registerFactoryParam<CakePayVerifyOtpPage, List<dynamic>, void>((List<dynamic> args, _) {
|
||||
final email = args.first as String;
|
||||
final isSignIn = args[1] as bool;
|
||||
|
||||
return CakePayVerifyOtpPage(getIt.get<CakePayAuthViewModel>(), email, isSignIn);
|
||||
});
|
||||
|
||||
getIt.registerFactoryParam<CakePayBuyCardPage, List<dynamic>, void>((List<dynamic> args, _) {
|
||||
final vendor = args.first as CakePayVendor;
|
||||
|
||||
return CakePayBuyCardPage(getIt.get<CakePayBuyCardViewModel>(param1: vendor));
|
||||
});
|
||||
|
||||
getIt.registerFactory(() => CakePayCardsPage(getIt.get<CakePayCardsListViewModel>()));
|
||||
|
||||
getIt.registerFactory(() => CakePayAccountPage(getIt.get<CakePayAccountViewModel>()));
|
||||
|
||||
getIt.registerFactory(() => AnonPayApi(
|
||||
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
|
||||
wallet: getIt.get<AppStore>().wallet!));
|
||||
@@ -1776,6 +1731,4 @@ Future<void> setup({
|
||||
initialToken: initialToken,
|
||||
),
|
||||
);
|
||||
|
||||
_isSetupFinished = true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/dfx/dfx_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/kryptonim/kryptonim.dart';
|
||||
import 'package:hash_wallet/buy/meld/meld_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart';
|
||||
import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/robinhood/robinhood_buy_provider.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
@@ -61,18 +56,12 @@ class ProvidersHelper {
|
||||
|
||||
static BuyProvider getProviderByType(ProviderType type) {
|
||||
switch (type) {
|
||||
case ProviderType.robinhood:
|
||||
return getIt.get<RobinhoodBuyProvider>();
|
||||
case ProviderType.dfx:
|
||||
return getIt.get<DFXBuyProvider>();
|
||||
case ProviderType.onramper:
|
||||
return getIt.get<OnRamperBuyProvider>();
|
||||
case ProviderType.moonpay:
|
||||
return getIt.get<MoonPayProvider>();
|
||||
case ProviderType.meld:
|
||||
return getIt.get<MeldBuyProvider>();
|
||||
case ProviderType.kriptonim:
|
||||
return getIt.get<KryptonimBuyProvider>();
|
||||
}
|
||||
// The remaining provider implementations (Robinhood/DFX/Onramper/Meld/
|
||||
// Kryptonim) were removed; their enum values stay for future re-enable.
|
||||
default:
|
||||
throw UnsupportedError('Buy provider ${type.title} is not available in this build');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,431 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class ChainflipExchangeProvider extends ExchangeProvider {
|
||||
ChainflipExchangeProvider();
|
||||
|
||||
static final List<CryptoCurrency> _supported = [
|
||||
CryptoCurrency.btc,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.usdc,
|
||||
CryptoCurrency.usdterc20,
|
||||
CryptoCurrency.flip,
|
||||
CryptoCurrency.wbtc,
|
||||
CryptoCurrency.sol,
|
||||
CryptoCurrency.usdcsol,
|
||||
CryptoCurrency.usdtSol,
|
||||
CryptoCurrency.arbEth,
|
||||
CryptoCurrency.usdcArb,
|
||||
CryptoCurrency.usdtArb,
|
||||
];
|
||||
|
||||
static const _baseURL = 'chainflip-broker.io';
|
||||
static const _assetsPath = '/assets';
|
||||
static const _quotePath = '/quotes-native';
|
||||
static const _swapPath = '/swap';
|
||||
static const _txInfoPath = '/status-by-deposit-channel';
|
||||
static const _affiliateBps = secrets.chainflipAffiliateFee;
|
||||
static const _affiliateKey = secrets.chainflipApiKey;
|
||||
|
||||
@override
|
||||
String get title => 'Chainflip';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => false;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description =>
|
||||
ExchangeProviderDescription.chainflip;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
|
||||
try {
|
||||
|
||||
if(!_supported.contains(from) || !_supported.contains(to)) {
|
||||
throw Exception('No rates found for $from to $to');
|
||||
}
|
||||
|
||||
final assetId = _normalizeCurrency(from);
|
||||
|
||||
final assetsResponse = await _getAssets();
|
||||
final assets = assetsResponse['assets'] as List<dynamic>;
|
||||
|
||||
final minAmount = assets.firstWhere(
|
||||
(asset) => asset['id'] == assetId,
|
||||
orElse: () => null)?['minimalAmountNative'] ?? '0';
|
||||
|
||||
if (minAmount == '0') throw Exception('No rates found for $from to $to');
|
||||
|
||||
return Limits(min: _amountFromNative(minAmount.toString(), from));
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
throw Exception('Chainflip failed to fetch limits');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount
|
||||
}) async {
|
||||
// TODO: It seems this rate is getting cached, and re-used for different amounts, can we not do this?
|
||||
|
||||
try {
|
||||
if (amount == 0) return 0.0;
|
||||
|
||||
if(!_supported.contains(from) || !_supported.contains(to)) return 0.0;
|
||||
|
||||
|
||||
final quoteParams = {
|
||||
'apiKey': _affiliateKey,
|
||||
'sourceAsset': _normalizeCurrency(from),
|
||||
'destinationAsset': _normalizeCurrency(to),
|
||||
'amount': _amountToNative(amount, from),
|
||||
'commissionBps': _affiliateBps
|
||||
};
|
||||
|
||||
final quoteResponse = await _getSwapQuote(quoteParams);
|
||||
|
||||
final expectedAmountOut =
|
||||
quoteResponse['egressAmountNative'] as String? ?? '0';
|
||||
|
||||
final rate = _amountFromNative(expectedAmountOut, to) / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'quoteParams': quoteParams,
|
||||
},
|
||||
responseData: {
|
||||
'expectedAmountOut': expectedAmountOut,
|
||||
'rate': rate,
|
||||
'quoteResponse': quoteResponse,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade(
|
||||
{required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll}) async {
|
||||
try {
|
||||
final maxSlippage = 2;
|
||||
|
||||
final quoteParams = {
|
||||
'apiKey': _affiliateKey,
|
||||
'sourceAsset': _normalizeCurrency(request.fromCurrency),
|
||||
'destinationAsset': _normalizeCurrency(request.toCurrency),
|
||||
'amount': _amountToNative(double.parse(request.fromAmount), request.fromCurrency),
|
||||
'commissionBps': _affiliateBps
|
||||
};
|
||||
|
||||
final quoteResponse = await _getSwapQuote(quoteParams);
|
||||
final estimatedPrice = quoteResponse['estimatedPrice'] as double;
|
||||
final minimumPrice = estimatedPrice * (100 - maxSlippage) / 100;
|
||||
|
||||
final swapParams = {
|
||||
'apiKey': _affiliateKey,
|
||||
'sourceAsset': _normalizeCurrency(request.fromCurrency),
|
||||
'destinationAsset': _normalizeCurrency(request.toCurrency),
|
||||
'destinationAddress': request.toAddress,
|
||||
'commissionBps': _affiliateBps,
|
||||
'minimumPrice': minimumPrice.toString(),
|
||||
'refundAddress': request.refundAddress,
|
||||
'boostFee': '6',
|
||||
'retryDurationInBlocks': '150'
|
||||
};
|
||||
|
||||
if (quoteResponse.containsKey('numberOfChunks') && quoteResponse.containsKey('chunkIntervalBlocks')) {
|
||||
swapParams.addAll({
|
||||
'numberOfChunks': quoteResponse['numberOfChunks'].toString(),
|
||||
'chunkIntervalBlocks': quoteResponse['chunkIntervalBlocks'].toString(),
|
||||
});
|
||||
}
|
||||
|
||||
final swapResponse = await _openDepositChannel(swapParams);
|
||||
|
||||
final id = '${swapResponse['issuedBlock']}-${swapResponse['network'].toString()}-${swapResponse['channelId']}';
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'quoteParams': quoteParams,
|
||||
'swapParams': swapParams,
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'inputAddress': swapResponse['address'].toString(),
|
||||
'estimatedPrice': estimatedPrice,
|
||||
'minimumPrice': minimumPrice,
|
||||
'swapResponse': swapResponse,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: swapResponse['address'].toString(),
|
||||
createdAt: DateTime.now(),
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: request.toAmount,
|
||||
state: TradeState.waiting,
|
||||
payoutAddress: request.toAddress,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
try {
|
||||
final channelParts = id.split('-');
|
||||
final network = channelParts[1];
|
||||
final normalizedNetwork = _normalizeNetworkName(network);
|
||||
|
||||
final statusParams = {
|
||||
'apiKey': _affiliateKey,
|
||||
'issuedBlock': channelParts[0],
|
||||
'network': normalizedNetwork,
|
||||
'channelId': channelParts[2]
|
||||
};
|
||||
|
||||
final statusResponse = await _getStatus(statusParams);
|
||||
|
||||
if (statusResponse == null)
|
||||
throw Exception('Trade not found for id: $id');
|
||||
|
||||
final status = statusResponse['status'];
|
||||
final currentState = _determineState(status['state'].toString());
|
||||
|
||||
final depositAmount = status['deposit']?['amount']?.toString() ?? '0.0';
|
||||
final receiveAmount = status['swapEgress']?['amount']?.toString() ?? '0.0';
|
||||
final refundAmount = status['refundEgress']?['amount']?.toString() ?? '0.0';
|
||||
final isRefund = status['refundEgress'] != null;
|
||||
final amount = isRefund ? refundAmount : receiveAmount;
|
||||
|
||||
final from = status['sourceAsset'].toString();
|
||||
final to = status['destinationAsset'].toString();
|
||||
|
||||
final newTrade = Trade(
|
||||
id: id,
|
||||
from: _toCurrency(from),
|
||||
to: _toCurrency(to),
|
||||
provider: description,
|
||||
amount: depositAmount,
|
||||
receiveAmount: amount,
|
||||
state: currentState,
|
||||
payoutAddress: status['destinationAddress'].toString(),
|
||||
outputTransaction:
|
||||
status['swapEgress']?['transactionReference']?.toString(),
|
||||
isRefund: isRefund,
|
||||
);
|
||||
|
||||
return newTrade;
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
final tag = currency.tag?.toLowerCase();
|
||||
final title = currency.title.toLowerCase();
|
||||
|
||||
// Naive assets without network tag
|
||||
if (tag == null) return '$title.$title';
|
||||
|
||||
return '$title.$tag';
|
||||
}
|
||||
|
||||
String _normalizeNetworkName (String name) {
|
||||
final networkName = switch (name) {
|
||||
'BITCOIN' => 'Bitcoin',
|
||||
'ETHEREUM' => 'Ethereum',
|
||||
'ARBITRUM' => 'Arbitrum',
|
||||
'SOLANA' => 'Solana',
|
||||
_ => name
|
||||
};
|
||||
|
||||
return networkName;
|
||||
}
|
||||
|
||||
|
||||
CryptoCurrency? _toCurrency(String name) {
|
||||
final currency = switch (name) {
|
||||
'btc.btc' => CryptoCurrency.btc,
|
||||
'eth.eth' => CryptoCurrency.eth,
|
||||
'usdc.eth' => CryptoCurrency.usdc,
|
||||
'usdt.eth' => CryptoCurrency.usdterc20,
|
||||
'flip.eth' => CryptoCurrency.flip,
|
||||
'wbtc.eth' => CryptoCurrency.wbtc,
|
||||
'sol.sol' => CryptoCurrency.sol,
|
||||
'usdc.sol' => CryptoCurrency.usdcsol,
|
||||
'eth.arb' => CryptoCurrency.arbEth,
|
||||
'usdc.arb' => CryptoCurrency.usdcArb,
|
||||
'usdt.arb' => CryptoCurrency.usdtArb,
|
||||
_ => null
|
||||
};
|
||||
|
||||
return currency;
|
||||
}
|
||||
|
||||
String _amountToNative(double amount, CryptoCurrency currency) =>
|
||||
(amount * pow(10, currency.decimals)).toInt().toString();
|
||||
|
||||
double _amountFromNative(String amount, CryptoCurrency currency) =>
|
||||
double.parse(amount) / pow(10, currency.decimals);
|
||||
|
||||
Future<Map<String, dynamic>> _getAssets() async =>
|
||||
_getRequest(_assetsPath, {});
|
||||
|
||||
Future<Map<String, dynamic>> _openDepositChannel(Map<String, String> params) async =>
|
||||
_getRequest(_swapPath, params);
|
||||
|
||||
Future<Map<String, dynamic>> _getRequest(String path, Map<String, String> params) async {
|
||||
final uri = Uri.https(_baseURL, path, params);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if ((response.statusCode != 200) || (response.body.contains('error'))) {
|
||||
throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}');
|
||||
}
|
||||
|
||||
return json.decode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
|
||||
final uri = Uri.https(_baseURL, _quotePath, params);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if ((response.statusCode != 200) || (response.body.contains('error'))) {
|
||||
throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}');
|
||||
}
|
||||
|
||||
final List<dynamic> jsonList = json.decode(response.body) as List<dynamic>;
|
||||
final List<Map<String, dynamic>> quotes =
|
||||
jsonList.map((e) => e as Map<String, dynamic>).toList();
|
||||
|
||||
Map<String, dynamic> highestQuote = quotes.reduce((current, next) {
|
||||
double currentAmount = current['egressAmount'] as double;
|
||||
double nextAmount = next['egressAmount'] as double;
|
||||
|
||||
return currentAmount > nextAmount ? current : next;
|
||||
});
|
||||
|
||||
return highestQuote;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _getStatus(Map<String, String> params) async {
|
||||
final uri = Uri.https(_baseURL, _txInfoPath, params);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode == 404) return null;
|
||||
|
||||
if ((response.statusCode != 200) || (response.body.contains('error'))) {
|
||||
throw Exception('Unexpected response: ${response.statusCode} / ${uri.toString()} / ${response.body}');
|
||||
}
|
||||
|
||||
return json.decode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
TradeState _determineState(String state) {
|
||||
final swapState = switch (state) {
|
||||
'waiting' => TradeState.waiting,
|
||||
'receiving' => TradeState.processing,
|
||||
'swapping' => TradeState.processing,
|
||||
'sending' => TradeState.processing,
|
||||
'sent' => TradeState.processing,
|
||||
'completed' => TradeState.success,
|
||||
'failed' => TradeState.failed,
|
||||
_ => TradeState.notFound
|
||||
};
|
||||
|
||||
return swapState;
|
||||
}
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:hash_wallet/utils/distribution_info.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hash_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class ChangeNowExchangeProvider extends ExchangeProvider {
|
||||
ChangeNowExchangeProvider({required SettingsStore settingsStore})
|
||||
: _settingsStore = settingsStore,
|
||||
_lastUsedRateId = '';
|
||||
|
||||
static final apiKey =
|
||||
isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey;
|
||||
static const apiAuthority = 'api.changenow.io';
|
||||
static const createTradePath = '/v2/exchange';
|
||||
static const findTradeByIdPath = '/v2/exchange/by-id';
|
||||
static const estimatedAmountPath = '/v2/exchange/estimated-amount';
|
||||
static const rangePath = '/v2/exchange/range';
|
||||
static const apiHeaderKey = 'x-changenow-api-key';
|
||||
|
||||
final SettingsStore _settingsStore;
|
||||
String _lastUsedRateId;
|
||||
|
||||
@override
|
||||
String get title => 'ChangeNOW';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.changeNow;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final headers = {apiHeaderKey: apiKey};
|
||||
final params = <String, String>{
|
||||
'fromCurrency': _normalizeCurrency(from),
|
||||
'toCurrency': _normalizeCurrency(to),
|
||||
'fromNetwork': _networkFor(from),
|
||||
'toNetwork': _networkFor(to),
|
||||
'flow': _getFlow(isFixedRateMode)
|
||||
};
|
||||
final uri = Uri.https(apiAuthority, rangePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
final min = double.tryParse(responseJSON['minAmount']?.toString() ?? '');
|
||||
final max = double.tryParse(responseJSON['maxAmount']?.toString() ?? '');
|
||||
if (max == 0) return null;
|
||||
return Limits(min: min, max: max);
|
||||
}
|
||||
|
||||
@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 = {apiHeaderKey: apiKey};
|
||||
final isReverse = isReceiveAmount;
|
||||
final type = isReverse ? 'reverse' : 'direct';
|
||||
final params = <String, String>{
|
||||
'fromCurrency': _normalizeCurrency(from),
|
||||
'toCurrency': _normalizeCurrency(to),
|
||||
'fromNetwork': _networkFor(from),
|
||||
'toNetwork': _networkFor(to),
|
||||
'type': type,
|
||||
'flow': _getFlow(isFixedRateMode)
|
||||
};
|
||||
|
||||
if (isReverse)
|
||||
params['toAmount'] = amount.toString();
|
||||
else
|
||||
params['fromAmount'] = amount.toString();
|
||||
|
||||
final uri = Uri.https(apiAuthority, estimatedAmountPath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final fromAmount = double.tryParse(responseJSON['fromAmount']?.toString() ?? '') ?? 0.0;
|
||||
final toAmount = double.tryParse(responseJSON['toAmount']?.toString() ?? '') ?? 0.0;
|
||||
if (fromAmount <= 0 || toAmount <= 0) return 0.0;
|
||||
final rateId = responseJSON['rateId'] as String? ?? '';
|
||||
|
||||
if (rateId.isNotEmpty) _lastUsedRateId = rateId;
|
||||
|
||||
final rate = isReverse ? (amount / fromAmount) : (toAmount / amount);
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'type': type,
|
||||
'flow': _getFlow(isFixedRateMode),
|
||||
},
|
||||
responseData: {
|
||||
'fromAmount': fromAmount,
|
||||
'toAmount': toAmount,
|
||||
'rateId': rateId,
|
||||
'rate': rate,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
final distributionPath = await DistributionInfo.instance.getDistributionPath();
|
||||
final formattedAppVersion = int.tryParse(_settingsStore.appVersion.replaceAll('.', '')) ?? 0;
|
||||
final payload = {
|
||||
'app': isMoneroOnly ? 'monerocom' : 'cakewallet',
|
||||
'device': Platform.operatingSystem,
|
||||
'distribution': distributionPath,
|
||||
'version': formattedAppVersion
|
||||
};
|
||||
final headers = {apiHeaderKey: apiKey, 'Content-Type': 'application/json'};
|
||||
final type = isFixedRateMode ? 'reverse' : 'direct';
|
||||
final body = <String, dynamic>{
|
||||
'fromCurrency': _normalizeCurrency(request.fromCurrency),
|
||||
'toCurrency': _normalizeCurrency(request.toCurrency),
|
||||
'fromNetwork': _networkFor(request.fromCurrency),
|
||||
'toNetwork': _networkFor(request.toCurrency),
|
||||
if (!isFixedRateMode) 'fromAmount': request.fromAmount,
|
||||
if (isFixedRateMode) 'toAmount': request.toAmount,
|
||||
'address': request.toAddress,
|
||||
'flow': _getFlow(isFixedRateMode),
|
||||
'type': type,
|
||||
'refundAddress': request.refundAddress,
|
||||
'payload': payload,
|
||||
};
|
||||
|
||||
if (isFixedRateMode) {
|
||||
// since we schedule to calculate the rate every 5 seconds we need to ensure that
|
||||
// we have the latest rate id with the given inputs before creating the trade
|
||||
await fetchRate(
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
amount: double.tryParse(request.toAmount) ?? 0,
|
||||
isFixedRateMode: true,
|
||||
isReceiveAmount: true,
|
||||
);
|
||||
body['rateId'] = _lastUsedRateId;
|
||||
}
|
||||
|
||||
final uri = Uri.https(apiAuthority, createTradePath);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
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<String, dynamic>;
|
||||
final id = responseJSON['id'] as String;
|
||||
final inputAddress = responseJSON['payinAddress'] as String;
|
||||
final refundAddress = responseJSON['refundAddress'] as String;
|
||||
final extraId = responseJSON['payinExtraId'] as String?;
|
||||
final payoutAddress = responseJSON['payoutAddress'] as String;
|
||||
final fromAmount = responseJSON['fromAmount']?.toString();
|
||||
final toAmount = responseJSON['toAmount']?.toString();
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
refundAddress: refundAddress,
|
||||
extraId: extraId,
|
||||
createdAt: DateTime.now(),
|
||||
amount: fromAmount ?? request.fromAmount,
|
||||
receiveAmount: toAmount ?? request.toAmount,
|
||||
state: TradeState.created,
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final headers = {apiHeaderKey: apiKey};
|
||||
final params = <String, String>{'id': id};
|
||||
final uri = Uri.https(apiAuthority, findTradeByIdPath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description);
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['message'] as String;
|
||||
|
||||
throw TradeNotFoundException(id, provider: description, description: error);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200)
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// Parsing 'from' currency
|
||||
final fromCurrency = responseJSON['fromCurrency'] as String;
|
||||
final fromNetwork = responseJSON['fromNetwork'] as String?;
|
||||
final _normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
|
||||
final fromTag = fromCurrency.toUpperCase() == _normalizedFromNetwork.toUpperCase()
|
||||
? null
|
||||
: _normalizedFromNetwork;
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
|
||||
|
||||
// Parsing 'to' currency
|
||||
final toCurrency = responseJSON['toCurrency'] as String;
|
||||
final toNetwork = responseJSON['toNetwork'] as String?;
|
||||
final _normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
|
||||
final toTag = toCurrency.toUpperCase() == _normalizedToNetwork.toUpperCase()
|
||||
? null
|
||||
: _normalizedToNetwork;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
|
||||
|
||||
final inputAddress = responseJSON['payinAddress'] as String;
|
||||
final expectedSendAmount = responseJSON['expectedAmountFrom'].toString();
|
||||
final status = responseJSON['status'] as String;
|
||||
final state = TradeState.deserialize(raw: status);
|
||||
final extraId = responseJSON['payinExtraId'] as String?;
|
||||
final outputTransaction = responseJSON['payoutHash'] as String?;
|
||||
final expiredAtRaw = responseJSON['validUntil'] as String?;
|
||||
final payoutAddress = responseJSON['payoutAddress'] as String;
|
||||
final expiredAt = DateTime.tryParse(expiredAtRaw ?? '')?.toLocal();
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
amount: expectedSendAmount,
|
||||
state: state,
|
||||
extraId: extraId,
|
||||
expiredAt: expiredAt,
|
||||
outputTransaction: outputTransaction,
|
||||
payoutAddress: payoutAddress,
|
||||
);
|
||||
}
|
||||
|
||||
String _getFlow(bool isFixedRate) => isFixedRate ? 'fixed-rate' : 'standard';
|
||||
|
||||
String _networkFor(CryptoCurrency currency) {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.usdt:
|
||||
return 'btc';
|
||||
case CryptoCurrency.arb:
|
||||
return 'arbitrum';
|
||||
default:
|
||||
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
if (currency.title == "USDC" && currency.tag == "POLY") {
|
||||
throw "Only Bridged USDC (USDC.e) is allowed in ChangeNow";
|
||||
}
|
||||
switch (currency) {
|
||||
case CryptoCurrency.zec:
|
||||
return 'zec';
|
||||
default:
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeTag(String tag) {
|
||||
switch (tag) {
|
||||
case 'POLY':
|
||||
return 'matic';
|
||||
case 'LN':
|
||||
return 'lightning';
|
||||
case 'AVAXC':
|
||||
return 'cchain';
|
||||
case 'ARB':
|
||||
return 'arbitrum';
|
||||
default:
|
||||
return tag.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeNetworkType(String network) {
|
||||
return switch (network.toUpperCase()) {
|
||||
'POLY' => 'MATIC',
|
||||
'AVAXC' => 'CCHAIN',
|
||||
'ARBITRUM' => 'ARB',
|
||||
_ => network,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/core/lightning_invoice_service.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class ExolixExchangeProvider extends ExchangeProvider {
|
||||
ExolixExchangeProvider();
|
||||
|
||||
static final apiKey = isMoneroOnly ? secrets.exolixMoneroApiKey : secrets.exolixCakeWalletApiKey;
|
||||
static const apiBaseUrl = 'exolix.com';
|
||||
static const transactionsPath = '/api/v2/transactions';
|
||||
static const ratePath = '/api/v2/rate';
|
||||
|
||||
@override
|
||||
String get title => 'Exolix';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.exolix;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
}) async {
|
||||
final params = <String, String>{
|
||||
'rateType': _getRateType(isFixedRateMode),
|
||||
'amount': '1',
|
||||
'apiToken': apiKey,
|
||||
};
|
||||
|
||||
if (isFixedRateMode) {
|
||||
params['coinFrom'] = _normalizeCurrency(to);
|
||||
params['coinTo'] = _normalizeCurrency(_overrideFromCryptoCurrency(from));
|
||||
params['networkFrom'] = _networkFor(to);
|
||||
params['networkTo'] = _networkFor(from);
|
||||
} else {
|
||||
params['coinFrom'] = _normalizeCurrency(_overrideFromCryptoCurrency(from));
|
||||
params['coinTo'] = _normalizeCurrency(to);
|
||||
params['networkFrom'] = _networkFor(from);
|
||||
params['networkTo'] = _networkFor(to);
|
||||
}
|
||||
|
||||
// Maximum of 2 attempts to fetch limits
|
||||
for (int i = 0; i < 2; i++) {
|
||||
final uri = Uri.https(apiBaseUrl, ratePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final minAmount = responseJSON['minAmount'];
|
||||
final maxAmount = responseJSON['maxAmount'];
|
||||
return Limits(min: _toDouble(minAmount), max: _toDouble(maxAmount));
|
||||
} else if (response.statusCode == 422) {
|
||||
final errorResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||
if (errorResponse.containsKey('minAmount')) {
|
||||
params['amount'] = errorResponse['minAmount'].toString();
|
||||
continue;
|
||||
}
|
||||
throw Exception('Error 422: ${errorResponse['message'] ?? 'Unknown error'}');
|
||||
} else {
|
||||
throw Exception('Unexpected HTTP status: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Failed to fetch limits after retrying.');
|
||||
}
|
||||
|
||||
@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 params = {
|
||||
'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(from)),
|
||||
'coinTo': _normalizeCurrency(to),
|
||||
'networkFrom': _networkFor(from),
|
||||
'networkTo': _networkFor(to),
|
||||
'rateType': _getRateType(isFixedRateMode),
|
||||
'apiToken': apiKey,
|
||||
};
|
||||
|
||||
if (isReceiveAmount)
|
||||
params['withdrawalAmount'] = amount.toString();
|
||||
else
|
||||
params['amount'] = amount.toString();
|
||||
|
||||
final uri = Uri.https(apiBaseUrl, ratePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final message = responseJSON['message'] as String?;
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception(message ?? 'Unknown error'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw Exception(message);
|
||||
}
|
||||
|
||||
final rate = double.tryParse(responseJSON['rate']?.toString() ?? '') ?? 0.0;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'rate': rate,
|
||||
'statusCode': response.statusCode,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
printV(s.toString());
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = {
|
||||
'coinFrom': _normalizeCurrency(_overrideFromCryptoCurrency(request.fromCurrency)),
|
||||
'coinTo': _normalizeCurrency(_overrideToCryptoCurrency(request.toCurrency, request.toAddress)),
|
||||
'networkFrom': _networkFor(request.fromCurrency),
|
||||
'networkTo': _networkFor(request.toCurrency),
|
||||
'withdrawalAddress': await _normalizeAddress(request.toAddress),
|
||||
'refundAddress': await _normalizeAddress(request.refundAddress),
|
||||
'rateType': _getRateType(isFixedRateMode),
|
||||
'apiToken': apiKey,
|
||||
};
|
||||
|
||||
if (isFixedRateMode)
|
||||
body['withdrawalAmount'] = request.toAmount;
|
||||
else
|
||||
body['amount'] = request.fromAmount;
|
||||
|
||||
final uri = Uri.https(apiBaseUrl, transactionsPath);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final errors = responseJSON['error'] as Map<String, String>;
|
||||
final errorMessage = errors.values.join(', ');
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception(errorMessage),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw Exception(errorMessage);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('Unexpected http status: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final id = responseJSON['id'] as String;
|
||||
final inputAddress = responseJSON['depositAddress'] as String;
|
||||
final refundAddress = responseJSON['refundAddress'] as String?;
|
||||
final extraId = responseJSON['depositExtraId'] as String?;
|
||||
final payoutAddress = responseJSON['withdrawalAddress'] as String;
|
||||
final amount = responseJSON['amount'].toString();
|
||||
final receiveAmount = responseJSON['amountTo']?.toString();
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'inputAddress': inputAddress,
|
||||
'refundAddress': refundAddress,
|
||||
'extraId': extraId,
|
||||
'payoutAddress': payoutAddress,
|
||||
'amount': amount,
|
||||
'receiveAmount': receiveAmount,
|
||||
'statusCode': response.statusCode,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
refundAddress: refundAddress,
|
||||
extraId: extraId,
|
||||
createdAt: DateTime.now(),
|
||||
amount: amount,
|
||||
receiveAmount: receiveAmount ?? request.toAmount,
|
||||
state: TradeState.created,
|
||||
payoutAddress: payoutAddress,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final findTradeByIdPath = '$transactionsPath/$id';
|
||||
final uri = Uri.https(apiBaseUrl, findTradeByIdPath);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode == 404) throw TradeNotFoundException(id, provider: description);
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final errors = responseJSON['errors'] as Map<String, String>;
|
||||
final errorMessage = errors.values.join(', ');
|
||||
|
||||
throw TradeNotFoundException(id, provider: description, description: errorMessage);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200)
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// Parsing 'from' currency
|
||||
final coinFrom = responseJSON['coinFrom']['coinCode'] as String;
|
||||
final coinFromNetwork = responseJSON['coinFrom']['network'] as String?;
|
||||
final _normalizedFromNetwork = _normalizeNetworkType(coinFromNetwork ?? '');
|
||||
final fromTag = coinFrom.toUpperCase() == _normalizedFromNetwork.toUpperCase() ? null : coinFromNetwork;
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(coinFrom, tag: fromTag);
|
||||
|
||||
// Parsing 'to' currency
|
||||
final coinTo = responseJSON['coinTo']['coinCode'] as String;
|
||||
final coinToNetwork = responseJSON['coinTo']['network'] as String?;
|
||||
final _normalizedToNetwork = _normalizeNetworkType(coinToNetwork ?? '');
|
||||
final toTag = coinTo.toUpperCase() == _normalizedToNetwork.toUpperCase() ? null : coinToNetwork;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(coinTo, tag: toTag);
|
||||
|
||||
final inputAddress = responseJSON['depositAddress'] as String;
|
||||
final amount = responseJSON['amount'].toString();
|
||||
final status = responseJSON['status'] as String;
|
||||
final extraId = responseJSON['depositExtraId'] as String?;
|
||||
final outputTransaction = responseJSON['hashOut']['hash'] as String?;
|
||||
final payoutAddress = responseJSON['withdrawalAddress'] as String;
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
amount: amount,
|
||||
state: TradeState.deserialize(raw: _prepareStatus(status)),
|
||||
extraId: extraId,
|
||||
outputTransaction: outputTransaction,
|
||||
payoutAddress: payoutAddress,
|
||||
);
|
||||
}
|
||||
|
||||
String _getRateType(bool isFixedRate) => isFixedRate ? 'fixed' : 'float';
|
||||
|
||||
String _prepareStatus(String status) {
|
||||
switch (status) {
|
||||
case 'deleted':
|
||||
case 'error':
|
||||
return 'overdue';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
String _networkFor(CryptoCurrency currency) {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.arb:
|
||||
return 'ARBITRUM';
|
||||
case CryptoCurrency.btcln:
|
||||
return 'LIGHTNING';
|
||||
default:
|
||||
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title;
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeNetworkType(String network) {
|
||||
return switch (network.toUpperCase()) {
|
||||
'ARBITRUM' => 'ARB',
|
||||
_ => network,
|
||||
};
|
||||
}
|
||||
|
||||
CryptoCurrency _overrideFromCryptoCurrency(CryptoCurrency currency) {
|
||||
if (currency == CryptoCurrency.zec)
|
||||
return CryptoCurrency.zaddr; // Sending is always shielded zcash
|
||||
return currency;
|
||||
}
|
||||
|
||||
CryptoCurrency _overrideToCryptoCurrency(CryptoCurrency currency, String address) {
|
||||
if (RegExp(r'u1[a-zA-Z0-9]{100,300}').hasMatch(address) && currency == CryptoCurrency.zec)
|
||||
return CryptoCurrency.zaddr; // If the user pastes a unified address use shielded zcash
|
||||
return currency;
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.nano:
|
||||
return 'XNO';
|
||||
case CryptoCurrency.bttc:
|
||||
return 'BTT';
|
||||
case CryptoCurrency.zec:
|
||||
return 'ZEC';
|
||||
case CryptoCurrency.zaddr:
|
||||
return 'ZEC-SHIELDED';
|
||||
default:
|
||||
return currency.title;
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeTag(String tag) {
|
||||
switch (tag) {
|
||||
case 'POLY':
|
||||
return 'Polygon';
|
||||
case 'ARB':
|
||||
return 'Arbitrum';
|
||||
default:
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _normalizeAddress(String address) async {
|
||||
if (address.startsWith('bitcoincash:'))
|
||||
return address.replaceFirst('bitcoincash:', '');
|
||||
|
||||
// Lightning addresses
|
||||
if(address.contains("@"))
|
||||
return await getBolt11FromLightingAddress(address) ?? address;
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
static double? _toDouble(dynamic value) {
|
||||
if (value is int) {
|
||||
return value.toDouble();
|
||||
} else if (value is double) {
|
||||
return value;
|
||||
} else if (value is String) {
|
||||
return double.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class LetsExchangeExchangeProvider extends ExchangeProvider {
|
||||
LetsExchangeExchangeProvider();
|
||||
|
||||
static const apiKey = secrets.letsExchangeBearerToken;
|
||||
static const _baseUrl = 'api.letsexchange.io';
|
||||
static const _infoPath = '/api/v1/info';
|
||||
static const _infoRevertPath = '/api/v1/info-revert';
|
||||
static const _createTransactionPath = '/api/v1/transaction';
|
||||
static const _createTransactionRevertPath = '/api/v1/transaction-revert';
|
||||
static const _getTransactionPath = '/api/v1/transaction';
|
||||
|
||||
static const _affiliateId = secrets.letsExchangeAffiliateId;
|
||||
|
||||
@override
|
||||
String get title => 'LetsExchange';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.letsExchange;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final networkFrom = _getNetworkType(from);
|
||||
final networkTo = _getNetworkType(to);
|
||||
|
||||
try {
|
||||
final params = {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
if (networkFrom != null) 'network_from': networkFrom,
|
||||
if (networkTo != null) 'network_to': networkTo,
|
||||
'amount': '1',
|
||||
'affiliate_id': _affiliateId,
|
||||
'float': isFixedRateMode ? 'false' : 'true',
|
||||
};
|
||||
|
||||
final responseJSON = await _getInfo(params, isFixedRateMode);
|
||||
final min = double.tryParse(responseJSON['min_amount'] as String);
|
||||
final max = double.tryParse(responseJSON['max_amount'] as String);
|
||||
return Limits(min: min, max: max);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
throw Exception('Failed to fetch limits');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount}) async {
|
||||
final networkFrom = _getNetworkType(from);
|
||||
final networkTo = _getNetworkType(to);
|
||||
try {
|
||||
final params = {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
if (networkFrom != null) 'network_from': networkFrom,
|
||||
if (networkTo != null) 'network_to': networkTo,
|
||||
'amount': amount.toString(),
|
||||
'affiliate_id': _affiliateId,
|
||||
'float': isFixedRateMode ? 'false' : 'true',
|
||||
};
|
||||
|
||||
final responseJSON = await _getInfo(params, isFixedRateMode);
|
||||
|
||||
final amountToGet = double.tryParse(responseJSON['amount'] as String) ?? 0.0;
|
||||
|
||||
if (amountToGet == 0.0) return 0.0;
|
||||
|
||||
final rate = isFixedRateMode ? amount / amountToGet : amountToGet / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'networkFrom': networkFrom,
|
||||
'networkTo': networkTo,
|
||||
'params': params,
|
||||
},
|
||||
responseData: {
|
||||
'amountToGet': amountToGet,
|
||||
'rate': rate,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'networkFrom': networkFrom,
|
||||
'networkTo': networkTo,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade(
|
||||
{required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll}) async {
|
||||
final networkFrom = _getNetworkType(request.fromCurrency);
|
||||
final networkTo = _getNetworkType(request.toCurrency);
|
||||
try {
|
||||
final params = {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
if (networkFrom != null) 'network_from': networkFrom,
|
||||
if (networkTo != null) 'network_to': networkTo,
|
||||
'amount': isFixedRateMode ? request.toAmount.toString() : request.fromAmount.toString(),
|
||||
'affiliate_id': _affiliateId,
|
||||
'float': isFixedRateMode ? 'false' : 'true',
|
||||
};
|
||||
|
||||
final responseInfoJSON = await _getInfo(params, isFixedRateMode);
|
||||
final rateId = responseInfoJSON['rate_id'] as String;
|
||||
|
||||
final withdrawalAddress = _normalizeBchAddress(request.toAddress);
|
||||
final returnAddress = _normalizeBchAddress(request.refundAddress);
|
||||
|
||||
final tradeParams = {
|
||||
'coin_from': request.fromCurrency.title,
|
||||
'coin_to': request.toCurrency.title,
|
||||
if (!isFixedRateMode) 'deposit_amount': request.fromAmount.toString(),
|
||||
'withdrawal': withdrawalAddress,
|
||||
if (isFixedRateMode) 'withdrawal_amount': request.toAmount.toString(),
|
||||
'withdrawal_extra_id': '',
|
||||
'return': returnAddress,
|
||||
'rate_id': rateId,
|
||||
if (networkFrom != null) 'network_from': networkFrom,
|
||||
if (networkTo != null) 'network_to': networkTo,
|
||||
'affiliate_id': _affiliateId,
|
||||
'float': isFixedRateMode ? 'false' : 'true',
|
||||
};
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': apiKey
|
||||
};
|
||||
|
||||
final uri = Uri.https(
|
||||
_baseUrl, isFixedRateMode ? _createTransactionRevertPath : _createTransactionPath);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(tradeParams),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('LetsExchange create trade failed: ${response.body}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'networkFrom': networkFrom,
|
||||
'networkTo': networkTo,
|
||||
'tradeParams': tradeParams,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('LetsExchange create trade failed: ${response.body}');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final id = responseJSON['transaction_id'] as String;
|
||||
final from = responseJSON['coin_from'] as String;
|
||||
final to = responseJSON['coin_to'] as String;
|
||||
final payoutAddress = responseJSON['withdrawal'] as String;
|
||||
final depositAddress = responseJSON['deposit'] as String;
|
||||
final refundAddress = responseJSON['return'] as String;
|
||||
final depositAmount = responseJSON['deposit_amount'] as String;
|
||||
final receiveAmount = responseJSON['withdrawal_amount'] as String;
|
||||
final status = responseJSON['status'] as String;
|
||||
|
||||
// We ignore the created_at from response and use DateTime.now() instead
|
||||
final createdAtString = responseJSON['created_at'] as String;
|
||||
final expiredAtTimestamp = responseJSON['expired_at'] as int;
|
||||
final extraId = responseJSON['deposit_extra_id'] as String?;
|
||||
|
||||
final createdAt = DateTime.now();
|
||||
final expiredAt = createdAt.add(Duration(minutes: 30));
|
||||
|
||||
CryptoCurrency fromCurrency;
|
||||
if (request.fromCurrency.tag != null && request.fromCurrency.title == from) {
|
||||
fromCurrency = request.fromCurrency;
|
||||
} else {
|
||||
fromCurrency = CryptoCurrency.fromString(from);
|
||||
}
|
||||
|
||||
CryptoCurrency toCurrency;
|
||||
if (request.toCurrency.tag != null && request.toCurrency.title == to) {
|
||||
toCurrency = request.toCurrency;
|
||||
} else {
|
||||
toCurrency = CryptoCurrency.fromString(to);
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'networkFrom': networkFrom,
|
||||
'networkTo': networkTo,
|
||||
'tradeParams': tradeParams,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'from': from,
|
||||
'to': to,
|
||||
'depositAddress': depositAddress,
|
||||
'payoutAddress': payoutAddress,
|
||||
'refundAddress': refundAddress,
|
||||
'depositAmount': depositAmount,
|
||||
'receiveAmount': receiveAmount,
|
||||
'status': status,
|
||||
'createdAt': createdAtString,
|
||||
'expiredAt': expiredAtTimestamp,
|
||||
'extraId': extraId,
|
||||
'statusCode': response.statusCode,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: fromCurrency,
|
||||
to: toCurrency,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
payoutAddress: payoutAddress,
|
||||
refundAddress: refundAddress,
|
||||
amount: depositAmount,
|
||||
receiveAmount: receiveAmount,
|
||||
state: TradeState.deserialize(raw: status),
|
||||
createdAt: createdAt,
|
||||
expiredAt: expiredAt,
|
||||
extraId: extraId,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'networkFrom': networkFrom,
|
||||
'networkTo': networkTo,
|
||||
},
|
||||
);
|
||||
log(e.toString());
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': apiKey
|
||||
};
|
||||
|
||||
final url = Uri.https(_baseUrl, '$_getTransactionPath/$id');
|
||||
final response = await ProxyWrapper().get(clearnetUri: url, headers: headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('LetsExchange fetch trade failed: ${response.body}');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
// Parsing 'from' currency
|
||||
final fromCurrency = responseJSON['coin_from'] as String;
|
||||
final fromNetwork = responseJSON['coin_from_network'] as String?;
|
||||
final normalizedFromNetwork = _normalizeNetworkType(fromNetwork ?? '');
|
||||
final fromTag = fromCurrency == normalizedFromNetwork ? null : normalizedFromNetwork;
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
|
||||
|
||||
// Parsing 'to' currency
|
||||
final toCurrency = responseJSON['coin_to'] as String;
|
||||
final toNetwork = responseJSON['coin_to_network'] as String?;
|
||||
final normalizedToNetwork = _normalizeNetworkType(toNetwork ?? '');
|
||||
final toTag = toCurrency == normalizedToNetwork ? null : normalizedToNetwork;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
|
||||
|
||||
final payoutAddress = responseJSON['withdrawal'] as String;
|
||||
final depositAddress = responseJSON['deposit'] as String;
|
||||
final refundAddress = responseJSON['return'] as String;
|
||||
final depositAmount = responseJSON['deposit_amount'] as String;
|
||||
final receiveAmount = responseJSON['withdrawal_amount'] as String;
|
||||
final status = responseJSON['status'] as String;
|
||||
|
||||
final extraId = responseJSON['deposit_extra_id'] as String?;
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
payoutAddress: payoutAddress,
|
||||
refundAddress: refundAddress,
|
||||
amount: depositAmount,
|
||||
receiveAmount: receiveAmount,
|
||||
state: TradeState.deserialize(raw: status),
|
||||
isRefund: status == 'refund',
|
||||
extraId: extraId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getInfo(Map<String, String> params, bool isFixedRateMode) async {
|
||||
final headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': apiKey
|
||||
};
|
||||
|
||||
try {
|
||||
final uri = Uri.https(_baseUrl, isFixedRateMode ? _infoRevertPath : _infoPath);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(params),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('LetsExchange fetch info failed: ${response.body}');
|
||||
}
|
||||
return json.decode(response.body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw Exception('LetsExchange failed to fetch info ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
String? _getNetworkType(CryptoCurrency currency) {
|
||||
if (currency.tag != null && currency.tag!.isNotEmpty) {
|
||||
switch (currency.tag!) {
|
||||
case 'TRX':
|
||||
return 'TRC20';
|
||||
case 'ETH':
|
||||
return 'ERC20';
|
||||
case 'BSC':
|
||||
return 'BEP20';
|
||||
case 'ARB':
|
||||
return 'ARBITRUM';
|
||||
default:
|
||||
return currency.tag!;
|
||||
}
|
||||
}
|
||||
|
||||
return _normalizeTitleToNetwork(currency.title);
|
||||
}
|
||||
|
||||
String _normalizeNetworkType(String network) {
|
||||
return switch (network.toUpperCase()) {
|
||||
'ERC20' => 'ETH',
|
||||
'TRC20' => 'TRX',
|
||||
'BEP20' => 'BSC',
|
||||
'ARBITRUM' => 'ARB',
|
||||
_ => network,
|
||||
};
|
||||
}
|
||||
|
||||
String _normalizeTitleToNetwork(String title) {
|
||||
return switch (title.toUpperCase()) {
|
||||
'ARB' => 'ARBITRUM',
|
||||
_ => title,
|
||||
};
|
||||
}
|
||||
|
||||
String _normalizeBchAddress(String address) =>
|
||||
address.startsWith('bitcoincash:') ? address.substring(12) : address;
|
||||
}
|
||||
@@ -1,479 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class SideShiftExchangeProvider extends ExchangeProvider {
|
||||
SideShiftExchangeProvider();
|
||||
|
||||
static const affiliateId = secrets.sideShiftAffiliateId;
|
||||
static const apiBaseUrl = 'https://sideshift.ai/api';
|
||||
static const rangePath = '/v2/pair';
|
||||
static const orderPath = '/v2/shifts';
|
||||
static const quotePath = '/v2/quotes';
|
||||
static const permissionPath = '/v2/permissions';
|
||||
|
||||
@override
|
||||
String get title => 'SideShift';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.sideShift;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async {
|
||||
const url = apiBaseUrl + permissionPath;
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode == 500) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
throw Exception('$error');
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) return false;
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
return responseJSON['createShift'] as bool;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final fromCurrency = isFixedRateMode ? to : from;
|
||||
final toCurrency = isFixedRateMode ? from : to;
|
||||
|
||||
final fromNetwork = _networkFor(fromCurrency);
|
||||
final toNetwork = _networkFor(toCurrency);
|
||||
|
||||
final url =
|
||||
"$apiBaseUrl$rangePath/${fromCurrency.title.toLowerCase()}-$fromNetwork/${toCurrency.title.toLowerCase()}-$toNetwork";
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode == 500) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
throw Exception('$error');
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final min = double.tryParse(responseJSON['min'] as String? ?? '');
|
||||
final max = double.tryParse(responseJSON['max'] as String? ?? '');
|
||||
|
||||
if (isFixedRateMode) {
|
||||
final currentRate = double.parse(responseJSON['rate'] as String);
|
||||
return Limits(
|
||||
min: min != null ? (min * currentRate) : null,
|
||||
max: max != null ? (max * currentRate) : null,
|
||||
);
|
||||
}
|
||||
|
||||
return Limits(min: min, max: max);
|
||||
}
|
||||
|
||||
@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 fromCurrency = from.title.toLowerCase();
|
||||
final toCurrency = to.title.toLowerCase();
|
||||
final depositNetwork = _networkFor(from);
|
||||
final settleNetwork = _networkFor(to);
|
||||
|
||||
final url =
|
||||
"$apiBaseUrl$rangePath/$fromCurrency-$depositNetwork/$toCurrency-$settleNetwork?amount=$amount";
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode == 500) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('SideShift Internal Server Error: $error'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'url': url,
|
||||
},
|
||||
);
|
||||
|
||||
throw Exception('SideShift Internal Server Error: $error');
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('Unexpected http status: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'url': url,
|
||||
},
|
||||
);
|
||||
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final rate = double.parse(responseJSON['rate'] as String);
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'url': url,
|
||||
},
|
||||
responseData: {
|
||||
'rate': rate,
|
||||
'statusCode': response.statusCode,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0.00;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
String url = '';
|
||||
final body = {
|
||||
'affiliateId': affiliateId,
|
||||
'settleAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
};
|
||||
|
||||
if (isFixedRateMode) {
|
||||
final quoteId = await _createQuote(request);
|
||||
body['quoteId'] = quoteId;
|
||||
|
||||
url = apiBaseUrl + orderPath + '/fixed';
|
||||
} else {
|
||||
url = apiBaseUrl + orderPath + '/variable';
|
||||
body["depositCoin"] = _normalizeCurrency(request.fromCurrency);
|
||||
body["settleCoin"] = _normalizeCurrency(request.toCurrency);
|
||||
body["settleNetwork"] = _networkFor(request.toCurrency);
|
||||
body["depositNetwork"] = _networkFor(request.fromCurrency);
|
||||
}
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description, description: error),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'url': url,
|
||||
'body': body,
|
||||
},
|
||||
);
|
||||
|
||||
throw TradeNotCreatedException(description, description: error);
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'url': url,
|
||||
'body': body,
|
||||
},
|
||||
);
|
||||
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final id = responseJSON['id'] as String;
|
||||
final inputAddress = responseJSON['depositAddress'] as String;
|
||||
final settleAddress = responseJSON['settleAddress'] as String;
|
||||
final depositAmount = responseJSON['depositAmount'] as String?;
|
||||
final depositMemo = responseJSON['depositMemo'] as String?;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'url': url,
|
||||
'body': body,
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'inputAddress': inputAddress,
|
||||
'settleAddress': settleAddress,
|
||||
'depositAmount': depositAmount,
|
||||
'depositMemo': depositMemo,
|
||||
'statusCode': response.statusCode,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
provider: description,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
inputAddress: inputAddress,
|
||||
refundAddress: settleAddress,
|
||||
state: TradeState.created,
|
||||
amount: depositAmount ?? request.fromAmount,
|
||||
receiveAmount: request.toAmount,
|
||||
payoutAddress: settleAddress,
|
||||
createdAt: DateTime.now(),
|
||||
isSendAll: isSendAll,
|
||||
extraId: depositMemo,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final url = apiBaseUrl + orderPath + '/' + id;
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
throw TradeNotFoundException(id, provider: description);
|
||||
}
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
throw TradeNotFoundException(id, provider: description, description: error);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final fromCurrency = responseJSON['depositCoin'] as String;
|
||||
final fromNetwork = responseJSON['depositNetwork'] as String?;
|
||||
final toCurrency = responseJSON['settleCoin'] as String;
|
||||
final toNetwork = responseJSON['settleNetwork'] as String?;
|
||||
final inputAddress = responseJSON['depositAddress'] as String;
|
||||
final expectedSendAmount = responseJSON['depositAmount'] as String?;
|
||||
final status = responseJSON['status'] as String?;
|
||||
final settleAddress = responseJSON['settleAddress'] as String;
|
||||
final isVariable = (responseJSON['type'] as String) == 'variable';
|
||||
final expiredAtRaw = responseJSON['expiresAt'] as String;
|
||||
final expiredAt = isVariable ? null : DateTime.tryParse(expiredAtRaw)?.toLocal();
|
||||
final depositMemo = responseJSON['depositMemo'] as String?;
|
||||
|
||||
final fromParsed = CryptoCurrency.safeParseCurrencyFromString(
|
||||
fromCurrency,
|
||||
tag: fromNetwork,
|
||||
);
|
||||
final toParsed = CryptoCurrency.safeParseCurrencyFromString(
|
||||
toCurrency,
|
||||
tag: toNetwork,
|
||||
);
|
||||
return Trade(
|
||||
id: id,
|
||||
from: fromParsed,
|
||||
to: toParsed,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
amount: expectedSendAmount ?? '',
|
||||
state: TradeState.deserialize(raw: status ?? 'created'),
|
||||
expiredAt: expiredAt,
|
||||
payoutAddress: settleAddress,
|
||||
extraId: depositMemo,
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _createQuote(TradeRequest request) async {
|
||||
final url = apiBaseUrl + quotePath;
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final body = {
|
||||
'depositCoin': _normalizeCurrency(request.fromCurrency),
|
||||
'settleCoin': _normalizeCurrency(request.toCurrency),
|
||||
'affiliateId': affiliateId,
|
||||
'settleAmount': request.toAmount,
|
||||
'settleNetwork': _networkFor(request.toCurrency),
|
||||
'depositNetwork': _networkFor(request.fromCurrency),
|
||||
};
|
||||
final uri = Uri.parse(url);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error']['message'] as String;
|
||||
|
||||
throw TradeNotCreatedException(description, description: error);
|
||||
}
|
||||
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
return responseJSON['id'] as String;
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.usdcEPoly:
|
||||
return 'usdc';
|
||||
default:
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _networkFor(CryptoCurrency currency) =>
|
||||
currency.tag != null ? _normalizeTag(currency.tag!) : 'mainnet';
|
||||
|
||||
String _normalizeTag(String tag) {
|
||||
switch (tag) {
|
||||
case 'ETH':
|
||||
return 'ethereum';
|
||||
case 'TRX':
|
||||
return 'tron';
|
||||
case 'LN':
|
||||
return 'lightning';
|
||||
case 'POL':
|
||||
return 'polygon';
|
||||
case 'ARB':
|
||||
return 'arbitrum';
|
||||
case 'ZEC':
|
||||
return 'zcash';
|
||||
case 'AVAXC':
|
||||
return 'avax';
|
||||
default:
|
||||
return tag.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeNetworkType(String network) {
|
||||
return switch (network) {
|
||||
'ethereum' => 'ETH',
|
||||
'tron' => 'TRX',
|
||||
'lightning' => 'LN',
|
||||
'polygon' => 'POL',
|
||||
'arbitrum' => 'ARB',
|
||||
'zcash' => 'ZEC',
|
||||
'avax' => 'AVAXC',
|
||||
_ => network,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/utils/device_info.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class SimpleSwapExchangeProvider extends ExchangeProvider {
|
||||
SimpleSwapExchangeProvider();
|
||||
|
||||
|
||||
static final apiKey =
|
||||
DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop;
|
||||
static const apiAuthority = 'api.simpleswap.io';
|
||||
static const getEstimatePath = '/v1/get_estimated';
|
||||
static const rangePath = '/v1/get_ranges';
|
||||
static const getExchangePath = '/v1/get_exchange';
|
||||
static const createExchangePath = '/v1/create_exchange';
|
||||
|
||||
@override
|
||||
String get title => 'SimpleSwap';
|
||||
|
||||
@override
|
||||
bool get isAvailable => false;
|
||||
|
||||
@override
|
||||
bool get isEnabled => false;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => false;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.simpleSwap;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async {
|
||||
final uri = Uri.https(apiAuthority, getEstimatePath, <String, String>{'api_key': apiKey});
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
return !(response.statusCode == 403);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final params = <String, dynamic>{
|
||||
'api_key': apiKey,
|
||||
'fixed': isFixedRateMode.toString(),
|
||||
'currency_from': _normalizeCurrency(from),
|
||||
'currency_to': _normalizeCurrency(to),
|
||||
};
|
||||
final uri = Uri.https(apiAuthority, rangePath, params);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode == 500) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['message'] as String;
|
||||
|
||||
throw Exception('$error');
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final min = double.tryParse(responseJSON['min'] as String? ?? '');
|
||||
final max = double.tryParse(responseJSON['max'] as String? ?? '');
|
||||
|
||||
return Limits(min: min, max: max);
|
||||
}
|
||||
|
||||
@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 params = {
|
||||
'api_key': apiKey,
|
||||
'currency_from': _normalizeCurrency(from),
|
||||
'currency_to': _normalizeCurrency(to),
|
||||
'amount': amount.toString(),
|
||||
'fixed': isFixedRateMode.toString()
|
||||
};
|
||||
final uri = Uri.https(apiAuthority, getEstimatePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.body == "null") {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('Null response body'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
return 0.00;
|
||||
}
|
||||
|
||||
final data = json.decode(response.body) as String;
|
||||
final rate = double.parse(data) / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'data': data,
|
||||
'rate': rate,
|
||||
'statusCode': response.statusCode,
|
||||
'responseBody': response.body,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
return 0.00;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
final headers = {'Content-Type': 'application/json'};
|
||||
final params = {'api_key': apiKey};
|
||||
final body = <String, dynamic>{
|
||||
"currency_from": _normalizeCurrency(request.fromCurrency),
|
||||
"currency_to": _normalizeCurrency(request.toCurrency),
|
||||
"amount": request.fromAmount,
|
||||
"fixed": isFixedRateMode,
|
||||
"user_refund_address": _normalizeAddress(request.refundAddress),
|
||||
"address_to": _normalizeAddress(request.toAddress)
|
||||
};
|
||||
final uri = Uri.https(apiAuthority, createExchangePath, params);
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['message'] as String;
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description, description: error),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw TradeNotCreatedException(description, description: error);
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final id = responseJSON['id'] as String;
|
||||
final inputAddress = responseJSON['address_from'] as String;
|
||||
final payoutAddress = responseJSON['address_to'] as String;
|
||||
final settleAddress = responseJSON['user_refund_address'] as String;
|
||||
final extraId = responseJSON['extra_id_from'] as String?;
|
||||
final receiveAmount = responseJSON['amount_to'] as String?;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'inputAddress': inputAddress,
|
||||
'payoutAddress': payoutAddress,
|
||||
'settleAddress': settleAddress,
|
||||
'extraId': extraId,
|
||||
'receiveAmount': receiveAmount,
|
||||
'statusCode': response.statusCode,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
provider: description,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
inputAddress: inputAddress,
|
||||
refundAddress: settleAddress,
|
||||
extraId: extraId,
|
||||
state: TradeState.created,
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: receiveAmount ?? request.toAmount,
|
||||
payoutAddress: payoutAddress,
|
||||
createdAt: DateTime.now(),
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final params = {'api_key': apiKey, 'id': id};
|
||||
final uri = Uri.https(apiAuthority, getExchangePath, params);
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
throw TradeNotFoundException(id, provider: description);
|
||||
}
|
||||
|
||||
if (response.statusCode == 400) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['message'] as String;
|
||||
|
||||
throw TradeNotFoundException(id, provider: description, description: error);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final fromCurrency = responseJSON['currency_from'] as String;
|
||||
final toCurrency = responseJSON['currency_to'] as String;
|
||||
final inputAddress = responseJSON['address_from'] as String;
|
||||
final expectedSendAmount = responseJSON['expected_amount'].toString();
|
||||
final extraId = responseJSON['extra_id_from'] as String?;
|
||||
final status = responseJSON['status'] as String;
|
||||
final payoutAddress = responseJSON['address_to'] as String;
|
||||
|
||||
final fromParsed =
|
||||
CryptoCurrency.safeParseCurrencyFromString(fromCurrency);
|
||||
final toParsed = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
|
||||
return Trade(
|
||||
id: id,
|
||||
from: fromParsed,
|
||||
to: toParsed,
|
||||
extraId: extraId,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
amount: expectedSendAmount,
|
||||
state: TradeState.deserialize(raw: status),
|
||||
payoutAddress: payoutAddress,
|
||||
);
|
||||
}
|
||||
|
||||
static String _normalizeCurrency(CryptoCurrency currency) {
|
||||
switch (currency) {
|
||||
case CryptoCurrency.zec:
|
||||
return 'zec';
|
||||
case CryptoCurrency.bnb:
|
||||
return currency.tag!.toLowerCase();
|
||||
case CryptoCurrency.usdterc20:
|
||||
return 'usdterc20';
|
||||
case CryptoCurrency.usdttrc20:
|
||||
return 'usdttrc20';
|
||||
case CryptoCurrency.usdcpoly:
|
||||
return 'usdcpoly';
|
||||
case CryptoCurrency.usdtPoly:
|
||||
return 'usdtpoly';
|
||||
case CryptoCurrency.usdcEPoly:
|
||||
return 'usdcepoly';
|
||||
case CryptoCurrency.usdcsol:
|
||||
return 'usdcspl';
|
||||
case CryptoCurrency.matic:
|
||||
return 'pol';
|
||||
case CryptoCurrency.maticpoly:
|
||||
return 'matic';
|
||||
default:
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
String _normalizeAddress(String address) =>
|
||||
address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class StealthExExchangeProvider extends ExchangeProvider {
|
||||
StealthExExchangeProvider();
|
||||
|
||||
static final apiKey = secrets.stealthExBearerToken;
|
||||
static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent);
|
||||
static const _baseUrl = 'https://api.stealthex.io';
|
||||
static const _rangePath = '/v4/rates/range';
|
||||
static const _amountPath = '/v4/rates/estimated-amount';
|
||||
static const _exchangesPath = '/v4/exchanges';
|
||||
|
||||
@override
|
||||
String get title => 'StealthEX';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.stealthEx;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final curFrom = isFixedRateMode ? to : from;
|
||||
final curTo = isFixedRateMode ? from : to;
|
||||
|
||||
final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'};
|
||||
final body = {
|
||||
'route': {
|
||||
'from': {'symbol': _getName(curFrom), 'network': _getNetwork(curFrom)},
|
||||
'to': {'symbol': _getName(curTo), 'network': _getNetwork(curTo)}
|
||||
},
|
||||
'estimation': isFixedRateMode ? 'reversed' : 'direct',
|
||||
'rate': isFixedRateMode ? 'fixed' : 'floating',
|
||||
'additional_fee_percent': _additionalFeePercent,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: Uri.parse(_baseUrl + _rangePath),
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('StealthEx fetch limits failed: ${response.body}');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final min = _toDouble(responseJSON['min_amount']);
|
||||
final max = _toDouble(responseJSON['max_amount']);
|
||||
return Limits(min: min, max: max);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
throw Exception('StealthEx failed to fetch limits');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount}) async {
|
||||
try {
|
||||
final response = await getEstimatedExchangeAmount(
|
||||
from: from,
|
||||
to: to,
|
||||
amount: amount,
|
||||
isFixedRateMode: isFixedRateMode,
|
||||
);
|
||||
final estimatedAmount = response['estimated_amount'] as double? ?? 0.0;
|
||||
|
||||
if (estimatedAmount <= 0.0) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('Invalid estimated amount: $estimatedAmount'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
final rate = isFixedRateMode ? amount / estimatedAmount : estimatedAmount / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
responseData: {
|
||||
'estimatedAmount': estimatedAmount,
|
||||
'rate': rate,
|
||||
'response': response,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade(
|
||||
{required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll}) async {
|
||||
String? rateId;
|
||||
String? validUntil;
|
||||
|
||||
try {
|
||||
if (isFixedRateMode) {
|
||||
final response = await getEstimatedExchangeAmount(
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
amount: double.parse(request.toAmount),
|
||||
isFixedRateMode: isFixedRateMode);
|
||||
rateId = response['rate_id'] as String?;
|
||||
validUntil = response['valid_until'] as String?;
|
||||
if (rateId == null) throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'};
|
||||
final body = {
|
||||
'route': {
|
||||
'from': {
|
||||
'symbol': _getName(request.fromCurrency),
|
||||
'network': _getNetwork(request.fromCurrency)
|
||||
},
|
||||
'to': {'symbol': _getName(request.toCurrency), 'network': _getNetwork(request.toCurrency)}
|
||||
},
|
||||
'estimation': isFixedRateMode ? 'reversed' : 'direct',
|
||||
'rate': isFixedRateMode ? 'fixed' : 'floating',
|
||||
if (isFixedRateMode) 'rate_id': rateId,
|
||||
'amount':
|
||||
isFixedRateMode ? double.parse(request.toAmount) : double.parse(request.fromAmount),
|
||||
'address': _normalizeAddress(request.toAddress),
|
||||
'refund_address': _normalizeAddress(request.refundAddress),
|
||||
'additional_fee_percent': _additionalFeePercent,
|
||||
};
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: Uri.parse(_baseUrl + _exchangesPath),
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('StealthEx create trade failed: ${response.body}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'rateId': rateId,
|
||||
},
|
||||
);
|
||||
throw Exception('StealthEx create trade failed: ${response.body}');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final deposit = responseJSON['deposit'] as Map<String, dynamic>;
|
||||
final withdrawal = responseJSON['withdrawal'] as Map<String, dynamic>;
|
||||
|
||||
final id = responseJSON['id'] as String;
|
||||
final from = deposit['symbol'] as String;
|
||||
final to = withdrawal['symbol'] as String;
|
||||
final payoutAddress = withdrawal['address'] as String;
|
||||
final depositAddress = deposit['address'] as String;
|
||||
final refundAddress = responseJSON['refund_address'] as String;
|
||||
final depositAmount = _toDouble(deposit['amount']);
|
||||
final receiveAmount = _toDouble(withdrawal['amount']);
|
||||
final status = responseJSON['status'] as String;
|
||||
final createdAtString = responseJSON['created_at'] as String;
|
||||
final extraId = deposit['extra_id'] as String?;
|
||||
|
||||
final createdAt = DateTime.parse(createdAtString).toLocal();
|
||||
final expiredAt = validUntil != null
|
||||
? DateTime.parse(validUntil).toLocal()
|
||||
: DateTime.now().add(Duration(minutes: 5));
|
||||
|
||||
CryptoCurrency fromCurrency;
|
||||
if (request.fromCurrency.tag != null && request.fromCurrency.title.toLowerCase() == from) {
|
||||
fromCurrency = request.fromCurrency;
|
||||
} else {
|
||||
fromCurrency = CryptoCurrency.fromString(from);
|
||||
}
|
||||
|
||||
CryptoCurrency toCurrency;
|
||||
if (request.toCurrency.tag != null && request.toCurrency.title.toLowerCase() == to) {
|
||||
toCurrency = request.toCurrency;
|
||||
} else {
|
||||
toCurrency = CryptoCurrency.fromString(to);
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'rateId': rateId,
|
||||
},
|
||||
responseData: {
|
||||
'id': id,
|
||||
'from': from,
|
||||
'to': to,
|
||||
'depositAddress': depositAddress,
|
||||
'payoutAddress': payoutAddress,
|
||||
'refundAddress': refundAddress,
|
||||
'depositAmount': depositAmount,
|
||||
'receiveAmount': receiveAmount,
|
||||
'status': status,
|
||||
'createdAt': createdAtString,
|
||||
'extraId': extraId,
|
||||
'statusCode': response.statusCode,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: fromCurrency,
|
||||
to: toCurrency,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
payoutAddress: payoutAddress,
|
||||
refundAddress: refundAddress,
|
||||
amount: depositAmount.toString(),
|
||||
receiveAmount: receiveAmount.toString(),
|
||||
state: TradeState.deserialize(raw: status),
|
||||
createdAt: createdAt,
|
||||
expiredAt: expiredAt,
|
||||
extraId: extraId,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
log(e.toString());
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'};
|
||||
|
||||
final uri = Uri.parse('$_baseUrl$_exchangesPath/$id');
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('StealthEx fetch trade failed: ${response.body}');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final deposit = responseJSON['deposit'] as Map<String, dynamic>;
|
||||
final withdrawal = responseJSON['withdrawal'] as Map<String, dynamic>;
|
||||
|
||||
final respId = responseJSON['id'] as String;
|
||||
|
||||
// Parsing 'from' currency with network tag
|
||||
final fromCurrency = deposit['symbol'] as String;
|
||||
final fromNetwork = deposit['network'] as String?;
|
||||
final fromTag = fromNetwork == 'mainnet' ? null : fromNetwork;
|
||||
final from = CryptoCurrency.safeParseCurrencyFromString(fromCurrency, tag: fromTag);
|
||||
|
||||
// Parsing 'to' currency with network tag
|
||||
final toCurrency = withdrawal['symbol'] as String;
|
||||
final toNetwork = withdrawal['network'] as String?;
|
||||
final toTag = toNetwork == 'mainnet' ? null : toNetwork;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency, tag: toTag);
|
||||
|
||||
final payoutAddress = withdrawal['address'] as String;
|
||||
final depositAddress = deposit['address'] as String;
|
||||
final refundAddress = responseJSON['refund_address'] as String;
|
||||
final depositAmount = _toDouble(deposit['amount']);
|
||||
final receiveAmount = _toDouble(withdrawal['amount']);
|
||||
final status = responseJSON['status'] as String;
|
||||
final createdAtString = responseJSON['created_at'] as String;
|
||||
final createdAt = DateTime.parse(createdAtString).toLocal();
|
||||
final extraId = deposit['extra_id'] as String?;
|
||||
|
||||
return Trade(
|
||||
id: respId,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
payoutAddress: payoutAddress,
|
||||
refundAddress: refundAddress,
|
||||
amount: depositAmount.toString(),
|
||||
receiveAmount: receiveAmount.toString(),
|
||||
state: TradeState.deserialize(raw: status),
|
||||
createdAt: createdAt,
|
||||
isRefund: status == 'refunded',
|
||||
extraId: extraId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getEstimatedExchangeAmount(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode}) async {
|
||||
final headers = {'Authorization': apiKey, 'Content-Type': 'application/json'};
|
||||
|
||||
final body = {
|
||||
'route': {
|
||||
'from': {'symbol': _getName(from), 'network': _getNetwork(from)},
|
||||
'to': {'symbol': _getName(to), 'network': _getNetwork(to)}
|
||||
},
|
||||
'estimation': isFixedRateMode ? 'reversed' : 'direct',
|
||||
'rate': isFixedRateMode ? 'fixed' : 'floating',
|
||||
'amount': amount,
|
||||
'additional_fee_percent': _additionalFeePercent,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: Uri.parse(_baseUrl + _amountPath),
|
||||
headers: headers,
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) return {};
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final rate = responseJSON['rate'] as Map<String, dynamic>?;
|
||||
return {
|
||||
'estimated_amount': responseJSON['estimated_amount'] as double?,
|
||||
if (rate != null) 'valid_until': rate['valid_until'] as String?,
|
||||
if (rate != null) 'rate_id': rate['id'] as String?
|
||||
};
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static double? _toDouble(dynamic value) {
|
||||
if (value is int) {
|
||||
return value.toDouble();
|
||||
} else if (value is double) {
|
||||
return value;
|
||||
} else if (value is String) {
|
||||
return double.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getName(CryptoCurrency currency) {
|
||||
if (currency == CryptoCurrency.usdcEPoly) return 'usdce';
|
||||
return currency.title.toLowerCase();
|
||||
}
|
||||
|
||||
String _getNetwork(CryptoCurrency currency) {
|
||||
if (currency == CryptoCurrency.arb || currency.tag == 'ARB') return 'arbitrum';
|
||||
if (currency.tag == null) return 'mainnet';
|
||||
|
||||
if (currency == CryptoCurrency.maticpoly) return 'mainnet';
|
||||
|
||||
if (currency.tag == 'POLY') return 'matic';
|
||||
|
||||
return currency.tag!.toLowerCase();
|
||||
}
|
||||
|
||||
String _normalizeAddress(String address) =>
|
||||
address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
|
||||
}
|
||||
@@ -1,865 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/amount_converter.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
|
||||
class SwapsXyzExchangeProvider extends ExchangeProvider {
|
||||
SwapsXyzExchangeProvider();
|
||||
|
||||
static final List<CryptoCurrency> _notSupportedAsSourceToken = [
|
||||
CryptoCurrency.sol,
|
||||
...CryptoCurrency.all.where(
|
||||
(c) => (c.tag ?? '').toUpperCase() == 'SOL' || c.tag == CryptoCurrency.bnb.tag,
|
||||
),
|
||||
];
|
||||
|
||||
static const _transferSig = '0xa9059cbb';
|
||||
static const _swapAndExecuteSig = '0x9be111d1';
|
||||
|
||||
static final _apiKey = secrets.swapsXyzApiKey;
|
||||
static const _baseUrl = 'api-v2.swaps.xyz';
|
||||
static const _getChainList = 'api/getChainList';
|
||||
static const _getPaths = 'api/getPaths';
|
||||
static const _getQuotePaths = 'api/getQuote';
|
||||
static const _getAction = 'api/getAction';
|
||||
static const _registerTxs = 'api/registerTxs';
|
||||
static const _getStatus = 'api/getStatus';
|
||||
|
||||
static final _headers = {'x-api-key': _apiKey};
|
||||
|
||||
static final _supportedChainList = <Chain>[];
|
||||
final Map<int, List<TokenPathInfo>> _tokensCache = {};
|
||||
|
||||
@override
|
||||
String get title => 'Swaps.XYZ';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
// There is an issue with fixed-rate swaps on Swaps.XYZ
|
||||
// when they don't guarantee that the trade will be created at the requested amount.
|
||||
@override
|
||||
bool get supportsFixedRate => false;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description =>
|
||||
ExchangeProviderDescription.swapsXyz;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
}) async {
|
||||
try {
|
||||
final chains = await _geSupportedChain();
|
||||
if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
|
||||
|
||||
final fromToUse = isFixedRateMode ? to : from;
|
||||
final toToUse = isFixedRateMode ? from : to;
|
||||
|
||||
final srcChain = _findChainByCurrency(fromToUse, chains);
|
||||
final dstChain = _findChainByCurrency(toToUse, chains);
|
||||
|
||||
await _ensureTokensCached(
|
||||
fromChain: srcChain, toChain: dstChain, from: fromToUse, to: toToUse);
|
||||
|
||||
final srcToken = _getTokenAddress(currency: fromToUse, chain: srcChain);
|
||||
final dstToken = _getTokenAddress(currency: toToUse, chain: dstChain);
|
||||
|
||||
final params = {
|
||||
'srcChainId': '${srcChain.chainId}',
|
||||
'srcToken': srcToken,
|
||||
'dstChainId': '${dstChain.chainId}',
|
||||
'dstToken': dstToken,
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseUrl, _getPaths, params);
|
||||
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('Unexpected http status: ${res.statusCode}');
|
||||
}
|
||||
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
|
||||
final paths =
|
||||
(body['paths'] as List? ?? const []).cast<Map<String, dynamic>>();
|
||||
if (paths.isEmpty) {
|
||||
throw Exception('No paths for ${fromToUse.title} -> ${toToUse.title}');
|
||||
}
|
||||
|
||||
final int requestedDstId = dstChain.chainId;
|
||||
|
||||
Map<String, dynamic> path = paths.firstWhere(
|
||||
(p) => p['chainId'] == requestedDstId,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (path.isEmpty) {
|
||||
path = paths.firstWhere(
|
||||
(p) => (p['tokens'] is List) || p['amountLimits'] != null,
|
||||
orElse: () => paths.first,
|
||||
);
|
||||
}
|
||||
|
||||
final supportsExactAmountIn =
|
||||
path['supportsExactAmountIn'] as bool? ?? false;
|
||||
final supportsExactAmountOut =
|
||||
path['supportsExactAmountOut'] as bool? ?? false;
|
||||
|
||||
if (isFixedRateMode && !supportsExactAmountOut) {
|
||||
throw Exception(
|
||||
'This route does not support fixed receive (exact-amount-out)');
|
||||
}
|
||||
if (!isFixedRateMode && !supportsExactAmountIn) {
|
||||
throw Exception(
|
||||
'This route does not support exact send (exact-amount-in)');
|
||||
}
|
||||
|
||||
Map<String, dynamic>? useLimits;
|
||||
|
||||
if (isFixedRateMode) {
|
||||
final tokensField = path['tokens'];
|
||||
useLimits = null;
|
||||
|
||||
if (tokensField is List && tokensField.isNotEmpty) {
|
||||
final tokens = tokensField.cast<Map<String, dynamic>>();
|
||||
String norm(String s) => s.toUpperCase();
|
||||
final wantSym = norm(_normalizeCakeNativeTokenName(toToUse.title));
|
||||
final wantAddr = (dstToken).toLowerCase();
|
||||
|
||||
final match = tokens.firstWhere(
|
||||
(t) {
|
||||
final sym = norm(t['symbol']?.toString() ?? '');
|
||||
final addr = (t['address']?.toString() ?? '').toLowerCase();
|
||||
return sym == wantSym || (addr.isNotEmpty && addr == wantAddr);
|
||||
},
|
||||
orElse: () => const <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (match.isNotEmpty) {
|
||||
useLimits = {
|
||||
'minAmount': match['minAmount'],
|
||||
'maxAmount': match['maxAmount'],
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Floating/Exact-in: use the route-level limits
|
||||
useLimits = path['amountLimits'] as Map<String, dynamic>?;
|
||||
}
|
||||
|
||||
final min = double.tryParse((useLimits?['minAmount'])?.toString() ?? '');
|
||||
final max = double.tryParse((useLimits?['maxAmount'])?.toString() ?? '');
|
||||
return Limits(min: min, max: max);
|
||||
} catch (e) {
|
||||
printV('fetchLimits error: $e');
|
||||
throw Exception('Error fetching limits: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<_PathInfo?> _pickPath({
|
||||
required int srcChainId,
|
||||
required String srcToken,
|
||||
required int dstChainId,
|
||||
required String dstToken,
|
||||
}) async {
|
||||
final uri = Uri.https(_baseUrl, _getPaths, {
|
||||
'srcChainId': '$srcChainId',
|
||||
'srcToken': srcToken,
|
||||
'dstChainId': '$dstChainId',
|
||||
'dstToken': dstToken,
|
||||
});
|
||||
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
if (res.statusCode != 200) return null;
|
||||
final body = json.decode(res.body) as Map<String, dynamic>;
|
||||
final paths =
|
||||
(body['paths'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
|
||||
if (paths.isEmpty) return null;
|
||||
final p = paths.first;
|
||||
return _PathInfo(
|
||||
supportsExactOut: p['supportsExactAmountOut'] == true,
|
||||
minToAmountHuman: (p['amountLimits']?['minAmount'] as String?) ?? '0',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount}) async {
|
||||
try {
|
||||
|
||||
if(_notSupportedAsSourceToken.contains(from) || _notSupportedAsSourceToken.contains(to)) {
|
||||
printV(
|
||||
'fetchRate: source token ${from.title} is not supported as source token');
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
final chains = await _geSupportedChain();
|
||||
if (chains.isEmpty) return 0.0;
|
||||
|
||||
final srcChain = _findChainByCurrency(from, chains);
|
||||
final dstChain = _findChainByCurrency(to, chains);
|
||||
|
||||
await _ensureTokensCached(
|
||||
fromChain: srcChain, toChain: dstChain, from: from, to: to);
|
||||
|
||||
final srcToken = _getTokenAddress(currency: from, chain: srcChain);
|
||||
final dstToken = _getTokenAddress(currency: to, chain: dstChain);
|
||||
|
||||
if (isReceiveAmount) {
|
||||
final path = await _pickPath(
|
||||
srcChainId: srcChain.chainId,
|
||||
srcToken: srcToken,
|
||||
dstChainId: dstChain.chainId,
|
||||
dstToken: dstToken,
|
||||
);
|
||||
if (path == null || !path.supportsExactOut) {
|
||||
printV(
|
||||
'fetchRate: route does not support exact-amount-out for ${from.title} -> ${to.title}');
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
final humanAmountStr = amount.toString();
|
||||
final formattedAmount = AmountConverter.toBaseUnits(
|
||||
humanAmountStr,
|
||||
isReceiveAmount ? to.decimals : from.decimals,
|
||||
);
|
||||
|
||||
final params = {
|
||||
'swapDirection':
|
||||
isReceiveAmount ? 'exact-amount-out' : 'exact-amount-in',
|
||||
'srcToken': srcToken,
|
||||
'dstToken': dstToken,
|
||||
'srcChainId': '${srcChain.chainId}',
|
||||
'dstChainId': '${dstChain.chainId}',
|
||||
'amount': formattedAmount,
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseUrl, _getQuotePaths, params);
|
||||
final response =
|
||||
await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
printV('fetchRate failed: ${response.body}');
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||
final exchangeRate = (data['exchangeRate'] as num?)?.toDouble() ?? 0.0;
|
||||
return exchangeRate;
|
||||
} catch (e) {
|
||||
printV('fetchRate error: $e');
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
try {
|
||||
final sender = request.refundAddress.trim();
|
||||
final recipient = request.toAddress.trim();
|
||||
if (sender.isEmpty || recipient.isEmpty) {
|
||||
throw Exception(
|
||||
'Sender (refundAddress) or recipient (toAddress) is empty');
|
||||
}
|
||||
|
||||
final chains = await _geSupportedChain();
|
||||
if (chains.isEmpty) throw Exception('Failed to fetch supported chains');
|
||||
final srcChain = _findChainByCurrency(request.fromCurrency, chains);
|
||||
final dstChain = _findChainByCurrency(request.toCurrency, chains);
|
||||
|
||||
await _ensureTokensCached(
|
||||
fromChain: srcChain,
|
||||
toChain: dstChain,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
);
|
||||
|
||||
final srcToken =
|
||||
_getTokenAddress(currency: request.fromCurrency, chain: srcChain);
|
||||
final dstToken =
|
||||
_getTokenAddress(currency: request.toCurrency, chain: dstChain);
|
||||
|
||||
final amountStr = isFixedRateMode ? request.toAmount : request.fromAmount;
|
||||
final rawAmount = double.tryParse(amountStr) ?? 0.0;
|
||||
if (rawAmount <= 0) throw Exception('Invalid amount');
|
||||
|
||||
final formattedAmount = AmountConverter.toBaseUnits(
|
||||
amountStr,
|
||||
isFixedRateMode
|
||||
? request.toCurrency.decimals
|
||||
: request.fromCurrency.decimals,
|
||||
);
|
||||
|
||||
final params = {
|
||||
'actionType': 'swap-action',
|
||||
'sender': sender,
|
||||
'srcChainId': '${srcChain.chainId}',
|
||||
'srcToken': srcToken,
|
||||
'dstChainId': '${dstChain.chainId}',
|
||||
'dstToken': dstToken,
|
||||
'slippage': '300',
|
||||
'swapDirection':
|
||||
isFixedRateMode ? 'exact-amount-out' : 'exact-amount-in',
|
||||
'amount': formattedAmount,
|
||||
'recipient': recipient,
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseUrl, _getAction, params);
|
||||
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('getAction failed: ${res.statusCode} ${res.body}');
|
||||
}
|
||||
|
||||
final data = json.decode(res.body) as Map<String, dynamic>;
|
||||
|
||||
final txId = data['txId'] as String? ?? '';
|
||||
|
||||
final vmId = data['vmId'] as String? ?? '';
|
||||
final txObj = (data['tx'] as Map?) ?? const {};
|
||||
|
||||
final txTo = txObj['to']?.toString();
|
||||
final chainId = txObj['chainId']?.toString();
|
||||
final routerData = txObj['data']?.toString();
|
||||
|
||||
// Allow only:
|
||||
// - null (native / deposit-address flow)
|
||||
// - '0x' (no call data)
|
||||
// - ERC20 transfer(0xa9059cbb) selector
|
||||
// - swapAndExecute(0x9be111d1) selector
|
||||
final isAllowed = routerData == null ||
|
||||
routerData == '0x' ||
|
||||
_decodeMethodSelector(routerData) == _transferSig ||
|
||||
_decodeMethodSelector(routerData) == _swapAndExecuteSig;
|
||||
|
||||
if (!isAllowed) {
|
||||
throw Exception('Does not support that method selector');
|
||||
}
|
||||
|
||||
final txValue = txObj['value']?.toString() ?? '0';
|
||||
|
||||
final bridgeIds = (data['bridgeIds'] as List?) ?? const [];
|
||||
if (txId.isEmpty) throw Exception('No txId returned by getAction');
|
||||
|
||||
final amtIn = (data['amountIn'] as Map?) ?? const {};
|
||||
final amtInMax = (data['amountInMax'] as Map?) ?? const {};
|
||||
final srcTokenAddr = amtIn['address']?.toString();
|
||||
final srcTokenDecs =
|
||||
(amtIn['decimals'] as num?)?.toInt() ?? request.fromCurrency.decimals;
|
||||
final requiresTokenApproval =
|
||||
data['requiresTokenApproval'] as bool? ?? false;
|
||||
|
||||
final reqAmountStr =
|
||||
(amtInMax['amount'] ?? amtIn['amount'])?.toString() ?? '0';
|
||||
final reqAmountRaw = reqAmountStr.replaceAll('n', '');
|
||||
|
||||
final needToRegisterInSwapXyz =
|
||||
vmId == 'alt-vm' || bridgeIds.contains('alt-vm')
|
||||
|| chainId == 'solana' || bridgeIds.contains('solana');
|
||||
|
||||
final trade = Trade(
|
||||
id: txId,
|
||||
router: chainId,
|
||||
providerId: vmId,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: txTo,
|
||||
refundAddress: request.refundAddress,
|
||||
state: TradeState.created,
|
||||
providerName: title,
|
||||
createdAt: DateTime.now(),
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: request.toAmount,
|
||||
payoutAddress: request.toAddress,
|
||||
isSendAll: isSendAll,
|
||||
needToRegisterInSwapXyz: needToRegisterInSwapXyz,
|
||||
sourceTokenAddress: srcTokenAddr ?? srcToken,
|
||||
sourceTokenDecimals: srcTokenDecs,
|
||||
sourceTokenAmountRaw: reqAmountRaw,
|
||||
requiresTokenApproval: requiresTokenApproval,
|
||||
routerData: routerData,
|
||||
routerValue: txValue,
|
||||
);
|
||||
|
||||
return trade;
|
||||
} catch (e) {
|
||||
printV('createTrade error: $e');
|
||||
throw TradeNotCreatedException(description, description: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a broadcasted tx with Swaps.xyz (required for alt-vm).
|
||||
static Future<bool> registerAltVmTx({
|
||||
required String txId,
|
||||
required String txHash,
|
||||
required int chainId,
|
||||
required String vmId,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.https(_baseUrl, _registerTxs);
|
||||
final payload = {
|
||||
'txId': txId,
|
||||
'vmId': vmId,
|
||||
'txHash': txHash,
|
||||
'chainId': chainId,
|
||||
};
|
||||
|
||||
final res = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: {
|
||||
..._headers,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
printV('registerTxs failed: ${res.statusCode} ${res.body}');
|
||||
return false;
|
||||
}
|
||||
final List<dynamic> body = json.decode(res.body) as List<dynamic>;
|
||||
if (body.isEmpty) return false;
|
||||
|
||||
final isSuccess =
|
||||
(body[0] as Map<String, dynamic>)['success'] as bool? ?? false;
|
||||
|
||||
return isSuccess;
|
||||
} catch (e) {
|
||||
printV('registerAltVmTx error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
if (id.isEmpty) {
|
||||
throw Exception('Trade id is empty');
|
||||
}
|
||||
|
||||
final uri = Uri.https(_baseUrl, _getStatus, {'txId': id});
|
||||
final resp = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception('getStatus failed: ${resp.statusCode} ${resp.body}');
|
||||
}
|
||||
|
||||
final data = json.decode(resp.body) as Map<String, dynamic>;
|
||||
final isSuccess = (data['success'] as bool?);
|
||||
|
||||
if (isSuccess != null && !isSuccess) {
|
||||
final error = data['error'] as Map<String, dynamic>?;
|
||||
if (error != null) {
|
||||
final code = error['code']?.toString() ?? 'unknown';
|
||||
throw Exception(
|
||||
'SwapXyzExchangeProvider findTradeById error: ($id) $code');
|
||||
}
|
||||
}
|
||||
|
||||
final statusStr = (data['status'] as String?)?.toLowerCase() ?? 'NOT_FOUND';
|
||||
final state = _mapSwapsStatusToTradeState(statusStr);
|
||||
|
||||
final refundAddress = data['sender']?.toString();
|
||||
|
||||
final srcTransaction = (data['srcTx'] as Map?)?.cast<String, dynamic>();
|
||||
final dstTransaction = (data['dstTx'] as Map?)?.cast<String, dynamic>();
|
||||
|
||||
final inputAddress = srcTransaction?['toAddress']?.toString();
|
||||
|
||||
final payoutAddress = dstTransaction?['toAddress']?.toString();
|
||||
|
||||
final srcPaymentToken =
|
||||
(srcTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
|
||||
final dstPaymentToken =
|
||||
(dstTransaction?['paymentToken'] as Map?)?.cast<String, dynamic>();
|
||||
|
||||
final fromSymbol = (srcPaymentToken?['symbol'] as String?) ?? '';
|
||||
final toSymbol = (dstPaymentToken?['symbol'] as String?) ?? '';
|
||||
|
||||
CryptoCurrency? toCurrency;
|
||||
if (toSymbol.isNotEmpty) {
|
||||
toCurrency = CryptoCurrency.safeParseCurrencyFromString(toSymbol);
|
||||
}
|
||||
|
||||
final srcDecimals = (srcPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
|
||||
final dstDecimals = (dstPaymentToken?['decimals'] as num?)?.toInt() ?? 0;
|
||||
|
||||
final txHash = srcTransaction?['txHash'] as String?;
|
||||
|
||||
// Minimal-unit amounts like "12000n"
|
||||
final srcAmountRaw = srcPaymentToken?['amount']?.toString();
|
||||
final dstAmountRaw = dstPaymentToken?['amount']?.toString();
|
||||
String? receiveAmount;
|
||||
if (dstAmountRaw != null) {
|
||||
final dstAmountMinimal = _stripN(dstAmountRaw);
|
||||
receiveAmount =
|
||||
AmountConverter.fromBaseUnits(dstAmountMinimal, dstDecimals);
|
||||
}
|
||||
|
||||
final srcAmountMinimal = _stripN(srcAmountRaw);
|
||||
final amount = AmountConverter.fromBaseUnits(srcAmountMinimal, srcDecimals);
|
||||
final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromSymbol);
|
||||
|
||||
// Timestamps can be num or "123n" handle both
|
||||
final srcTs = _parseUnixSeconds(srcTransaction?['timestamp']);
|
||||
final dstTs = _parseUnixSeconds(dstTransaction?['timestamp']);
|
||||
final timestamp = srcTs ?? dstTs;
|
||||
|
||||
final createdAt = timestamp != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true)
|
||||
.toLocal()
|
||||
: null;
|
||||
|
||||
return Trade(
|
||||
id: (data['txId'] as String?) ?? id,
|
||||
from: fromCurrency,
|
||||
to: toCurrency,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
payoutAddress: payoutAddress,
|
||||
amount: amount,
|
||||
receiveAmount: receiveAmount,
|
||||
txId: txHash,
|
||||
state: state,
|
||||
createdAt: createdAt,
|
||||
refundAddress: refundAddress,
|
||||
);
|
||||
}
|
||||
|
||||
TradeState _mapSwapsStatusToTradeState(String s) {
|
||||
switch (s) {
|
||||
case 'pending':
|
||||
case 'processing':
|
||||
return TradeState.pending;
|
||||
case 'success':
|
||||
case 'completed':
|
||||
case 'complete':
|
||||
return TradeState.finished;
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
case 'error':
|
||||
return TradeState.failed;
|
||||
default:
|
||||
return TradeState.pending;
|
||||
}
|
||||
}
|
||||
|
||||
// Load & cache supported chains once
|
||||
Future<List<Chain>> _geSupportedChain() async {
|
||||
if (_supportedChainList.isNotEmpty) return _supportedChainList;
|
||||
try {
|
||||
final uri = Uri.https(_baseUrl, _getChainList);
|
||||
final response =
|
||||
await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
if (response.statusCode != 200) return [];
|
||||
|
||||
final data = json.decode(response.body) as List<dynamic>;
|
||||
_supportedChainList
|
||||
..clear()
|
||||
..addAll(data.map((e) => Chain.fromJson(e as Map<String, dynamic>)));
|
||||
return _supportedChainList;
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ensureTokensCached({
|
||||
required Chain fromChain,
|
||||
required Chain toChain,
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
}) async {
|
||||
final needSrc = !_tokensCache.containsKey(fromChain.chainId) ||
|
||||
(_tokensCache[fromChain.chainId]?.isEmpty ?? true);
|
||||
|
||||
final needDst = !_tokensCache.containsKey(toChain.chainId) ||
|
||||
(_tokensCache[toChain.chainId]?.isEmpty ?? true);
|
||||
|
||||
if (!needSrc && !needDst) return;
|
||||
|
||||
if (needSrc) {
|
||||
await _fetchAndCacheTokens(srcChainId: fromChain.chainId);
|
||||
}
|
||||
if (needDst) {
|
||||
await _fetchAndCacheTokens(srcChainId: toChain.chainId);
|
||||
}
|
||||
}
|
||||
|
||||
// call getPaths and merge tokens into cache keyed by the chainId
|
||||
Future<void> _fetchAndCacheTokens({
|
||||
required int srcChainId,
|
||||
}) async {
|
||||
final params = <String, String>{
|
||||
'srcChainId': '$srcChainId',
|
||||
'srcToken': '0x0000000000000000000000000000000000000000',
|
||||
// Native placeholder
|
||||
};
|
||||
|
||||
final uri = Uri.https(_baseUrl, _getPaths, params);
|
||||
final res = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
if (res.statusCode != 200) {
|
||||
printV('getPaths failed: ${res.statusCode} ${res.body}');
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, dynamic> body;
|
||||
try {
|
||||
body = json.decode(res.body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
printV('getPaths JSON decode error: $e');
|
||||
return;
|
||||
}
|
||||
|
||||
// Always cache the source chain's native token (from body['srcToken'])
|
||||
final srcTokenJson = body['srcToken'] as Map<String, dynamic>?;
|
||||
if (srcTokenJson != null) {
|
||||
final symbol = (srcTokenJson['symbol'] as String? ?? '').toUpperCase();
|
||||
if (symbol.isNotEmpty) {
|
||||
final isNative = srcTokenJson['isNative'] == true;
|
||||
final decimals = (srcTokenJson['decimals'] as num?)?.toInt();
|
||||
// Treat native token as address = null so _getTokenAddress() emits zero-address
|
||||
final addr = isNative ? null : (srcTokenJson['address'] as String?);
|
||||
_mergeCache(srcChainId, [
|
||||
TokenPathInfo(
|
||||
symbol: symbol,
|
||||
address: addr,
|
||||
decimals: decimals,
|
||||
minAmount: srcTokenJson['minAmount']?.toString(),
|
||||
maxAmount: srcTokenJson['maxAmount']?.toString(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
final paths = (body['paths'] as List?) ?? const [];
|
||||
if (paths.isEmpty) return;
|
||||
|
||||
for (final path in paths) {
|
||||
final map = path as Map<String, dynamic>;
|
||||
final pathChainId = (map['chainId'] as num?)?.toInt();
|
||||
if (pathChainId == null) continue;
|
||||
|
||||
final tokensField = map['tokens'];
|
||||
|
||||
// Case 1: String "all" -> cache empty list to indicate all tokens supported
|
||||
if (tokensField is String) {
|
||||
if (tokensField.toLowerCase() == 'all') {
|
||||
_tokensCache[pathChainId] =
|
||||
_tokensCache[pathChainId] ?? <TokenPathInfo>[];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: List -> parse and merge
|
||||
if (tokensField is List) {
|
||||
final parsed = <TokenPathInfo>[];
|
||||
for (final token in tokensField) {
|
||||
if (token is Map<String, dynamic>) {
|
||||
try {
|
||||
parsed.add(TokenPathInfo.fromJson(token));
|
||||
} catch (e) {
|
||||
printV('Token parse error on chain $pathChainId: $e : $token');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed.isNotEmpty) {
|
||||
_mergeCache(pathChainId, parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge by symbol, prefer entries that have a non-empty address/decimals
|
||||
void _mergeCache(int chainId, List<TokenPathInfo> incoming) {
|
||||
final existing = _tokensCache[chainId] ?? const <TokenPathInfo>[];
|
||||
final bySymbol = <String, TokenPathInfo>{
|
||||
for (final t in existing) t.symbol: t
|
||||
};
|
||||
|
||||
for (final t in incoming) {
|
||||
final cur = bySymbol[t.symbol];
|
||||
if (cur == null) {
|
||||
bySymbol[t.symbol] = t;
|
||||
} else {
|
||||
// If incoming has a real address/decimals, prefer it
|
||||
final hasBetterAddr = (t.address != null && t.address!.isNotEmpty) &&
|
||||
(cur.address == null || cur.address!.isEmpty);
|
||||
final hasBetterDec = (t.decimals != null) && (cur.decimals == null);
|
||||
if (hasBetterAddr || hasBetterDec) {
|
||||
bySymbol[t.symbol] = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_tokensCache[chainId] = bySymbol.values.toList();
|
||||
}
|
||||
|
||||
String _normalizeCakeNativeTokenName(String title) {
|
||||
final name = title.toUpperCase();
|
||||
return switch (name) {
|
||||
'ZZEC' => 'ZEC',
|
||||
_ => name,
|
||||
};
|
||||
}
|
||||
|
||||
String _getTokenAddress({
|
||||
required CryptoCurrency currency,
|
||||
required Chain chain,
|
||||
}) {
|
||||
final symbol = _normalizeCakeNativeTokenName(currency.title);
|
||||
final list = _tokensCache[chain.chainId];
|
||||
|
||||
// Try cache hit
|
||||
if (list != null && list.isNotEmpty) {
|
||||
for (final t in list) {
|
||||
if (t.symbol == symbol && t.address != null && t.address!.isNotEmpty) {
|
||||
return t.address!;
|
||||
} else if (t.symbol == symbol && (t.address == null)) {
|
||||
// Native token on this chain
|
||||
return '0x0000000000000000000000000000000000000000';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// May fail for non-native Alt-VM assets
|
||||
return symbol;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? findTokenBySymbol(
|
||||
{required String title, required List<dynamic> tokens}) {
|
||||
final reqSymbol = title.toUpperCase();
|
||||
for (final token in tokens) {
|
||||
final map = token as Map<String, dynamic>;
|
||||
final symbol = (map['symbol'] as String?)?.toUpperCase();
|
||||
if (symbol == reqSymbol) return map;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Chain _findChainByCurrency(CryptoCurrency cur, List<Chain> chains) {
|
||||
final network = _normalizeCakeNetwork(cur.tag ?? cur.title);
|
||||
return chains.firstWhere(
|
||||
(c) {
|
||||
return c.name.toUpperCase() == network;
|
||||
},
|
||||
orElse: () => throw Exception('Unsupported chain for ${cur.title}'),
|
||||
);
|
||||
}
|
||||
|
||||
String _normalizeCakeNetwork(String network) {
|
||||
return switch (network.toUpperCase()) {
|
||||
'ETH' => 'ETHEREUM',
|
||||
'BSC' => 'BNB SMART CHAIN',
|
||||
'POL' => 'POLYGON',
|
||||
'AVAXC' => 'AVALANCHE',
|
||||
'TRX' => 'TRON',
|
||||
'SOL' => 'SOLANA',
|
||||
'CRO' => 'CRONOS',
|
||||
'ADA' => 'CARDANO',
|
||||
'KAS' => 'KASPA',
|
||||
'TON' => 'TONCOIN',
|
||||
'BCH' => 'BITCOIN CASH',
|
||||
'ARB' => 'ARBITRUM',
|
||||
_ => network.toUpperCase(),
|
||||
};
|
||||
}
|
||||
|
||||
int? _parseUnixSeconds(dynamic value) {
|
||||
if (value == null) return null;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) {
|
||||
final clean = _stripN(value);
|
||||
return int.tryParse(clean);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _stripN(String? str) {
|
||||
final s = str ?? '0';
|
||||
return s.endsWith('n') ? s.substring(0, s.length - 1) : s;
|
||||
}
|
||||
|
||||
String _decodeMethodSelector(String s) =>
|
||||
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
|
||||
}
|
||||
|
||||
class TokenPathInfo {
|
||||
final String symbol;
|
||||
final String? address;
|
||||
final int? decimals;
|
||||
final String? minAmount;
|
||||
final String? maxAmount;
|
||||
|
||||
TokenPathInfo({
|
||||
required this.symbol,
|
||||
required this.address,
|
||||
required this.decimals,
|
||||
required this.minAmount,
|
||||
required this.maxAmount,
|
||||
});
|
||||
|
||||
factory TokenPathInfo.fromJson(Map<String, dynamic> json) => TokenPathInfo(
|
||||
symbol: (json['symbol'] as String?)?.toUpperCase() ?? '',
|
||||
address: json['address'] as String?,
|
||||
decimals: json['decimals'] as int?,
|
||||
minAmount: json['minAmount']?.toString(),
|
||||
maxAmount: json['maxAmount']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
class Chain {
|
||||
final int chainId;
|
||||
final String name;
|
||||
final String vmId;
|
||||
|
||||
Chain({
|
||||
required this.chainId,
|
||||
required this.name,
|
||||
required this.vmId,
|
||||
});
|
||||
|
||||
factory Chain.fromJson(Map<String, dynamic> json) {
|
||||
return Chain(
|
||||
chainId: json['chainId'] as int,
|
||||
name: json['name'] as String,
|
||||
vmId: json['vmId'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PathInfo {
|
||||
final bool supportsExactOut;
|
||||
final String minToAmountHuman;
|
||||
|
||||
_PathInfo({required this.supportsExactOut, required this.minToAmountHuman});
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_found_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class SwapTradeExchangeProvider extends ExchangeProvider {
|
||||
SwapTradeExchangeProvider();
|
||||
|
||||
static final markup = secrets.swapTradeExchangeMarkup;
|
||||
|
||||
static const apiAuthority = 'api.swaptrade.io';
|
||||
static const getRate = '/api/swap/get-rate';
|
||||
static const getCoins = '/api/swap/get-coins';
|
||||
static const createOrder = '/api/swap/create-order';
|
||||
static const order = '/api/swap/order';
|
||||
|
||||
@override
|
||||
String get title => 'SwapTrade';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => false;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.swapTrade;
|
||||
|
||||
static const _headers = <String, String>{'Content-Type': 'application/json'};
|
||||
|
||||
@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 ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode != 200)
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
|
||||
final coinsInfoRaw = responseJSON['data'];
|
||||
final coinsInfo = coinsInfoRaw is List<dynamic> ? coinsInfoRaw : <dynamic>[];
|
||||
|
||||
final normalized = _normalizeCurrency(from);
|
||||
final coin = coinsInfo.cast<Map<String, dynamic>>().firstWhere(
|
||||
(c) => (c['id']?.toString().toUpperCase() ?? '') == normalized,
|
||||
orElse: () => <String, dynamic>{},
|
||||
);
|
||||
|
||||
if (coin.isEmpty) {
|
||||
// Currency not supported by SwapTrade (e.g. USDC, DOGE).
|
||||
return null;
|
||||
}
|
||||
|
||||
final min = double.tryParse(coin['min']?.toString() ?? '') ?? 0.0;
|
||||
final max = double.tryParse(coin['max']?.toString() ?? '') ?? 0.0;
|
||||
if (max == 0) return null;
|
||||
return Limits(min: min, max: max);
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
if (isFixedRateMode && !supportsFixedRate) {
|
||||
return 0.0;
|
||||
}
|
||||
if(from == CryptoCurrency.btcln || to == CryptoCurrency.btcln) return 0;
|
||||
|
||||
final params = <String, dynamic>{};
|
||||
final body = <String, String>{
|
||||
'coin_send': _normalizeCurrency(from),
|
||||
'coin_receive': _normalizeCurrency(to),
|
||||
'amount': amount.toString(),
|
||||
'ref': 'cake',
|
||||
};
|
||||
|
||||
final uri = Uri.https(apiAuthority, getRate, params);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
body: json.encode(body),
|
||||
headers: _headers,
|
||||
);
|
||||
|
||||
final responseBody = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('Unexpected http status: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final data = responseBody['data'] as Map<String, dynamic>;
|
||||
double rate = double.parse(data['price'].toString());
|
||||
final calculatedRate = rate > 0 ? isFixedRateMode ? amount / rate : rate / amount : 0.0;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'rate': rate,
|
||||
'calculatedRate': calculatedRate,
|
||||
'statusCode': response.statusCode,
|
||||
'responseBody': responseBody,
|
||||
},
|
||||
);
|
||||
|
||||
return calculatedRate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV("error fetching rate: ${e.toString()}");
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
try {
|
||||
final params = <String, dynamic>{};
|
||||
var body = <String, dynamic>{
|
||||
'coin_send': _normalizeCurrency(request.fromCurrency),
|
||||
'coin_send_network': _networkFor(request.fromCurrency),
|
||||
'coin_receive': _normalizeCurrency(request.toCurrency),
|
||||
'coin_receive_network': _networkFor(request.toCurrency),
|
||||
'amount_send': request.fromAmount,
|
||||
'recipient': request.toAddress,
|
||||
'ref': 'cake',
|
||||
'markup': double.tryParse(markup.toString()) ?? 0,
|
||||
'refund_address': request.refundAddress,
|
||||
};
|
||||
|
||||
final uri = Uri.https(apiAuthority, createOrder, params);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
body: json.encode(body),
|
||||
headers: _headers,
|
||||
);
|
||||
|
||||
final responseBody = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (response.statusCode == 400 || responseBody["success"] == false) {
|
||||
final List<dynamic> errorsList = responseBody['errors'] as List? ?? [];
|
||||
final error = errorsList.isNotEmpty
|
||||
? (errorsList[0]['msg'] as String?) ?? responseBody.toString()
|
||||
: responseBody.toString();
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description, description: error),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw TradeNotCreatedException(description, description: error);
|
||||
}
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('Unexpected http status: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('Unexpected http status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseData = responseBody['data'] as Map<String, dynamic>;
|
||||
final receiveAmount = responseData["amount_receive"]?.toString();
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'body': body,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'id': responseData["order_id"] as String,
|
||||
'inputAddress': responseData["server_address"] as String,
|
||||
'receiveAmount': receiveAmount,
|
||||
'statusCode': response.statusCode,
|
||||
'responseBody': responseBody,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: responseData["order_id"] as String,
|
||||
inputAddress: responseData["server_address"] as String,
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: receiveAmount ?? request.toAmount,
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
createdAt: DateTime.now(),
|
||||
state: TradeState.created,
|
||||
payoutAddress: request.toAddress,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
printV("error creating trade: ${e.toString()}");
|
||||
throw TradeNotCreatedException(description, description: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
try {
|
||||
final params = <String, dynamic>{};
|
||||
var body = <String, dynamic>{
|
||||
'order_id': id,
|
||||
};
|
||||
|
||||
final uri = Uri.https(apiAuthority, order, params);
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
body: json.encode(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.safeParseCurrencyFromString(fromCurrency);
|
||||
final toCurrency = responseData['coin_receive'] as String;
|
||||
final to = CryptoCurrency.safeParseCurrencyFromString(toCurrency);
|
||||
final inputAddress = responseData['server_address'] as String;
|
||||
final payoutAddress = responseData['recipient'] 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;
|
||||
final expectedReceiveAmount = responseData['amount_receive'] as String;
|
||||
final memo = responseData['memo'] as String?;
|
||||
final createdAt = responseData['created_at'] as String?;
|
||||
|
||||
return Trade(
|
||||
id: response_id,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
amount: expectedSendAmount,
|
||||
payoutAddress: payoutAddress,
|
||||
state: state,
|
||||
receiveAmount: expectedReceiveAmount,
|
||||
memo: memo,
|
||||
createdAt: DateTime.tryParse(createdAt ?? ''),
|
||||
);
|
||||
} catch (e) {
|
||||
printV("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) {
|
||||
final network = switch (currency) {
|
||||
CryptoCurrency.eth => 'ETH',
|
||||
CryptoCurrency.bnb => 'BNB_BSC',
|
||||
CryptoCurrency.usdterc20 => 'USDT_ERC20',
|
||||
CryptoCurrency.usdttrc20 => 'TRX_USDT_S2UZ',
|
||||
CryptoCurrency.usdtbsc => 'USDT_BSC',
|
||||
CryptoCurrency.sol => 'SOL',
|
||||
CryptoCurrency.btc => 'BTC',
|
||||
CryptoCurrency.xmr => 'XMR',
|
||||
CryptoCurrency.ltc => 'LTC',
|
||||
CryptoCurrency.ada => 'ADA',
|
||||
CryptoCurrency.bch => 'BCH',
|
||||
CryptoCurrency.zec => 'ZEC',
|
||||
_ => currency.title.toUpperCase(),
|
||||
};
|
||||
return network;
|
||||
}
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
|
||||
class ThorChainExchangeProvider extends ExchangeProvider {
|
||||
ThorChainExchangeProvider();
|
||||
|
||||
static final isRefundAddressSupported = [CryptoCurrency.eth];
|
||||
|
||||
static const _baseNodeURL = 'thornode.ninerealms.com';
|
||||
static const _baseURL = 'midgard.ninerealms.com';
|
||||
static const _quotePath = '/thorchain/quote/swap';
|
||||
static const _txInfoPath = '/thorchain/tx/status/';
|
||||
static const _affiliateName = 'cakewallet';
|
||||
static const _affiliateBps = '175';
|
||||
static const _nameLookUpPath = 'v2/thorname/lookup/';
|
||||
|
||||
@override
|
||||
String get title => 'THORChain';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => false;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.thorChain;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
@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 params = {
|
||||
'from_asset': _normalizeCurrency(from),
|
||||
'to_asset': _normalizeCurrency(to),
|
||||
'amount': _doubleToThorChainString(amount),
|
||||
'affiliate': _affiliateName,
|
||||
'affiliate_bps': _affiliateBps
|
||||
};
|
||||
|
||||
final responseJSON = await _getSwapQuote(params);
|
||||
|
||||
final expectedAmountOut = responseJSON['expected_amount_out'] as String? ?? '0.0';
|
||||
final rate = _thorChainAmountToDouble(expectedAmountOut) / amount;
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
'params': params,
|
||||
},
|
||||
responseData: {
|
||||
'expectedAmountOut': expectedAmountOut,
|
||||
'rate': rate,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return rate;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Limits?> fetchLimits(
|
||||
{required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode}) async {
|
||||
final params = {
|
||||
'from_asset': _normalizeCurrency(from),
|
||||
'to_asset': _normalizeCurrency(to),
|
||||
'amount': _doubleToThorChainString(1),
|
||||
'affiliate': _affiliateName,
|
||||
'affiliate_bps': _affiliateBps
|
||||
};
|
||||
|
||||
final responseJSON = await _getSwapQuote(params);
|
||||
final minAmountIn = responseJSON['recommended_min_amount_in'] as String? ?? '0.0';
|
||||
|
||||
return Limits(min: _thorChainAmountToDouble(minAmountIn));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
|
||||
|
||||
final formattedFromAmount = double.parse(request.fromAmount);
|
||||
|
||||
final params = {
|
||||
'from_asset': _normalizeCurrency(request.fromCurrency),
|
||||
'to_asset': _normalizeCurrency(request.toCurrency),
|
||||
'amount': _doubleToThorChainString(formattedFromAmount),
|
||||
'destination': _normalizeAddress(request.toAddress),
|
||||
'affiliate': _affiliateName,
|
||||
'affiliate_bps': _affiliateBps,
|
||||
'refund_address':
|
||||
isRefundAddressSupported.contains(request.fromCurrency) ? _normalizeAddress(request.refundAddress) : '',
|
||||
};
|
||||
|
||||
final responseJSON = await _getSwapQuote(params);
|
||||
|
||||
final inputAddress = responseJSON['inbound_address'] as String?;
|
||||
final memo = responseJSON['memo'] as String?;
|
||||
final directAmountOutResponse = responseJSON['expected_amount_out'] as String?;
|
||||
|
||||
String? receiveAmount;
|
||||
if (directAmountOutResponse != null) {
|
||||
receiveAmount = _thorChainAmountToDouble(directAmountOutResponse).toString();
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'params': params,
|
||||
},
|
||||
responseData: {
|
||||
'inputAddress': inputAddress,
|
||||
'memo': memo,
|
||||
'directAmountOutResponse': directAmountOutResponse,
|
||||
'receiveAmount': receiveAmount,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: '',
|
||||
from: request.fromCurrency,
|
||||
to: request.toCurrency,
|
||||
provider: description,
|
||||
inputAddress: inputAddress,
|
||||
createdAt: DateTime.now(),
|
||||
amount: request.fromAmount,
|
||||
receiveAmount: receiveAmount ?? request.toAmount,
|
||||
state: TradeState.notFound,
|
||||
payoutAddress: request.toAddress,
|
||||
memo: memo,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
if (id.isEmpty) throw Exception('Trade id is empty');
|
||||
final formattedId = id.startsWith('0x') ? id.substring(2) : id;
|
||||
final uri = Uri.https(_baseNodeURL, '$_txInfoPath$formattedId');
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
|
||||
if (response.statusCode == 404) {
|
||||
throw Exception('Trade not found for id: $formattedId');
|
||||
} else if (response.statusCode != 200) {
|
||||
throw Exception('Unexpected HTTP status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final responseJSON = json.decode(response.body);
|
||||
final Map<String, dynamic> stagesJson = responseJSON['stages'] as Map<String, dynamic>;
|
||||
|
||||
final inboundObservedStarted = stagesJson['inbound_observed']?['started'] as bool? ?? true;
|
||||
if (!inboundObservedStarted) {
|
||||
throw Exception('Trade has not started for id: $formattedId');
|
||||
}
|
||||
|
||||
final currentState = _updateStateBasedOnStages(stagesJson) ?? TradeState.notFound;
|
||||
|
||||
final tx = responseJSON['tx'];
|
||||
final String fromAddress = tx['from_address'] as String? ?? '';
|
||||
final String toAddress = tx['to_address'] as String? ?? '';
|
||||
final List<dynamic> coins = tx['coins'] as List<dynamic>;
|
||||
final String? memo = tx['memo'] as String?;
|
||||
|
||||
final parts = memo?.split(':') ?? [];
|
||||
|
||||
final String toChain = parts.length > 1 ? parts[1].split('.')[0] : '';
|
||||
final String toAsset = parts.length > 1 && parts[1].split('.').length > 1
|
||||
? parts[1].split('.')[1].split('-')[0]
|
||||
: '';
|
||||
|
||||
final formattedToChain = CryptoCurrency.safeParseCurrencyFromString(toChain);
|
||||
final toAssetWithChain = CryptoCurrency.safeParseCurrencyFromString(toAsset, walletCurrency: formattedToChain);
|
||||
|
||||
final plannedOutTxs = responseJSON['planned_out_txs'] as List<dynamic>?;
|
||||
final isRefund = plannedOutTxs?.any((tx) => tx['refund'] == true) ?? false;
|
||||
|
||||
return Trade(
|
||||
id: id,
|
||||
from: CryptoCurrency.fromString(tx['chain'] as String? ?? ''),
|
||||
to: toAssetWithChain,
|
||||
provider: description,
|
||||
inputAddress: fromAddress,
|
||||
payoutAddress: toAddress,
|
||||
amount: coins.first['amount'] as String? ?? '0.0',
|
||||
state: currentState,
|
||||
memo: memo,
|
||||
isRefund: isRefund,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?>? lookupAddressByName(String name) async {
|
||||
final uri = Uri.https(_baseURL, '$_nameLookUpPath$name');
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final body = json.decode(response.body) as Map<String, dynamic>;
|
||||
final entries = body['entries'] as List<dynamic>?;
|
||||
|
||||
if (entries == null || entries.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, String> chainToAddressMap = {};
|
||||
|
||||
for (final entry in entries) {
|
||||
final chain = entry['chain'] as String;
|
||||
final address = entry['address'] as String;
|
||||
chainToAddressMap[chain] = address;
|
||||
}
|
||||
|
||||
return chainToAddressMap;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _getSwapQuote(Map<String, String> params) async {
|
||||
Uri uri = Uri.https(_baseNodeURL, _quotePath, params);
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: '_getSwapQuote',
|
||||
error: Exception('Unexpected HTTP status: ${response.statusCode}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('Unexpected HTTP status: ${response.statusCode}');
|
||||
}
|
||||
|
||||
if (response.body.contains('error')) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: '_getSwapQuote',
|
||||
error: Exception('Unexpected response: ${response.body}'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'params': params,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
throw Exception('Unexpected response: ${response.body}');
|
||||
}
|
||||
|
||||
return json.decode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
String _normalizeCurrency(CryptoCurrency currency) {
|
||||
final networkTitle = currency.tag == 'ETH' ? 'ETH' : currency.tag ?? currency.title;
|
||||
return '$networkTitle.${currency.title}';
|
||||
}
|
||||
|
||||
String _doubleToThorChainString(double amount) => (amount * 1e8).toInt().toString();
|
||||
|
||||
double _thorChainAmountToDouble(String amount) => double.parse(amount) / 1e8;
|
||||
|
||||
TradeState? _updateStateBasedOnStages(Map<String, dynamic> stages) {
|
||||
TradeState? currentState;
|
||||
|
||||
if (stages['inbound_observed']['completed'] as bool? ?? false) {
|
||||
currentState = TradeState.confirmation;
|
||||
}
|
||||
if (stages['inbound_confirmation_counted']['completed'] as bool? ?? false) {
|
||||
currentState = TradeState.confirmed;
|
||||
}
|
||||
if (stages['inbound_finalised']['completed'] as bool? ?? false) {
|
||||
currentState = TradeState.processing;
|
||||
}
|
||||
if (stages['swap_finalised']['completed'] as bool? ?? false) {
|
||||
currentState = TradeState.traded;
|
||||
}
|
||||
if (stages['outbound_signed']['completed'] as bool? ?? false) {
|
||||
currentState = TradeState.success;
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
String _normalizeAddress(String address) =>
|
||||
address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
|
||||
}
|
||||
@@ -1,565 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_not_created_exception.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/utils/package_info.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/utils/proxy_wrapper.dart';
|
||||
import 'package:hash_wallet/utils/exchange_provider_logger.dart';
|
||||
class XOSwapExchangeProvider extends ExchangeProvider {
|
||||
XOSwapExchangeProvider() {
|
||||
_addAppVersionHeader();
|
||||
}
|
||||
|
||||
void _addAppVersionHeader() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.version;
|
||||
_headers['App-Version'] = currentVersion;
|
||||
}
|
||||
|
||||
static const _apiAuthority = 'exchange.exodus.io';
|
||||
static const _apiPath = '/v3';
|
||||
static const _pairsPath = '/pairs';
|
||||
static const _ratePath = '/rates';
|
||||
static const _orders = '/orders';
|
||||
static const _assets = '/assets';
|
||||
|
||||
static final _headers = {'Content-Type': 'application/json', 'App-Name': 'cake-labs'};
|
||||
|
||||
final _networks = <String, String>{
|
||||
'POL': 'matic',
|
||||
'ETH': 'ethereum',
|
||||
'BTC': 'bitcoin',
|
||||
'BSC': 'bsc',
|
||||
'SOL': 'solana',
|
||||
'TRX': 'tronmainnet',
|
||||
'ZEC': 'zcash',
|
||||
'ADA': 'cardano',
|
||||
'DOGE': 'dogecoin',
|
||||
'XMR': 'monero',
|
||||
'BCH': 'bcash',
|
||||
'BSV': 'bitcoinsv',
|
||||
'XRP': 'ripple',
|
||||
'LTC': 'litecoin',
|
||||
'EOS': 'eosio',
|
||||
'XLM': 'stellar',
|
||||
'BASE': 'basemainnet',
|
||||
'ARB': 'arbitrum',
|
||||
};
|
||||
|
||||
static const supportedTags = [
|
||||
'POL',
|
||||
'ETH',
|
||||
'BTC',
|
||||
'BSC',
|
||||
'SOL',
|
||||
'TRX',
|
||||
'ZEC',
|
||||
'ADA',
|
||||
'DOGE',
|
||||
'XMR',
|
||||
'BCH',
|
||||
'BSV',
|
||||
'XRP',
|
||||
'LTC',
|
||||
'EOS',
|
||||
'XLM',
|
||||
'BASE',
|
||||
'ARB',
|
||||
];
|
||||
|
||||
|
||||
String _normalizeXOSwapsNetwork(String string) {
|
||||
final lower = string.toLowerCase();
|
||||
|
||||
if (lower.endsWith('matic0a883d9b')) return string.replaceFirst(RegExp(r'matic0a883d9b$', caseSensitive: false), 'POL');
|
||||
if (lower.endsWith('matic86e249c1')) return string.replaceFirst(RegExp(r'matic86e249c1$', caseSensitive: false), 'POL');
|
||||
if (lower.endsWith('bscddedf0f8')) return string.replaceFirst(RegExp(r'bscddedf0f8$', caseSensitive: false), 'BSC');
|
||||
if (lower.endsWith('basemainnetb5a52617')) return string.replaceFirst(RegExp(r'basemainnetb5a52617$', caseSensitive: false), 'BASE');
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
@override
|
||||
String get title => 'XOSwap';
|
||||
|
||||
@override
|
||||
bool get isAvailable => true;
|
||||
|
||||
@override
|
||||
bool get isEnabled => true;
|
||||
|
||||
@override
|
||||
bool get supportsFixedRate => true;
|
||||
|
||||
@override
|
||||
ExchangeProviderDescription get description => ExchangeProviderDescription.xoSwap;
|
||||
|
||||
@override
|
||||
Future<bool> checkIsAvailable() async => true;
|
||||
|
||||
Future<String?> _getAssets(CryptoCurrency currency) async {
|
||||
if (currency.tag == null) return currency.title;
|
||||
try {
|
||||
final normalizedNetwork = _networks[currency.tag];
|
||||
if (normalizedNetwork == null) return null;
|
||||
|
||||
final uri = Uri.https(_apiAuthority, _apiPath + _assets,
|
||||
{'networks': normalizedNetwork, 'query': currency.title});
|
||||
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to fetch assets for ${currency.title} on ${currency.tag}');
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! List) throw const FormatException('Unexpected response format');
|
||||
final assets = decoded.map((e) => Map<String, dynamic>.from(e as Map)).toList();
|
||||
|
||||
|
||||
final asset = assets.firstWhere(
|
||||
(asset) => removeNonAlphanumeric((asset['symbol'] ?? '').toString()) == currency.title,
|
||||
orElse: () => const {},
|
||||
);
|
||||
|
||||
return asset.isEmpty ? null : asset['id'] as String;
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String removeNonAlphanumeric(String str) => str.toUpperCase().replaceAll(RegExp(r'[^A-Z0-9]'), '');
|
||||
|
||||
Future<List<dynamic>> getRatesForPair({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
}) async {
|
||||
try {
|
||||
final curFrom = await _getAssets(from);
|
||||
final curTo = await _getAssets(to);
|
||||
if (curFrom == null || curTo == null) return [];
|
||||
final pairId = curFrom + '_' + curTo;
|
||||
final uri = Uri.https(_apiAuthority, '$_apiPath$_pairsPath/$pairId$_ratePath');
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (response.statusCode != 200) return [];
|
||||
return json.decode(response.body) as List<dynamic>;
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Limits?> fetchLimits({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required bool isFixedRateMode,
|
||||
}) async {
|
||||
try {
|
||||
final rates = await getRatesForPair(from: from, to: to);
|
||||
if (rates.isEmpty) throw Exception('No rates found for $from to $to');
|
||||
|
||||
double minLimit = double.infinity;
|
||||
double maxLimit = 0;
|
||||
|
||||
for (var rate in rates) {
|
||||
final double currentMin = double.parse(rate['min']['value'].toString());
|
||||
final double currentMax = double.parse(rate['max']['value'].toString());
|
||||
if (currentMin < minLimit) minLimit = currentMin;
|
||||
if (currentMax > maxLimit) maxLimit = currentMax;
|
||||
}
|
||||
return Limits(min: minLimit, max: maxLimit);
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
throw Exception('StealthEx failed to fetch limits');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<double> fetchRate({
|
||||
required CryptoCurrency from,
|
||||
required CryptoCurrency to,
|
||||
required double amount,
|
||||
required bool isFixedRateMode,
|
||||
required bool isReceiveAmount
|
||||
}) async {
|
||||
try {
|
||||
final rates = await getRatesForPair(from: from, to: to);
|
||||
if (rates.isEmpty) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: Exception('No rates found for $from to $to'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
double result;
|
||||
if (!isFixedRateMode) {
|
||||
double bestOutput = 0.0;
|
||||
for (var rate in rates) {
|
||||
final double minVal = double.parse(rate['min']['value'].toString());
|
||||
final double maxVal = double.parse(rate['max']['value'].toString());
|
||||
if (amount >= minVal && amount <= maxVal) {
|
||||
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
|
||||
final double minerFee = double.parse(rate['minerFee']['value'].toString());
|
||||
final double outputAmount = (amount * rateMultiplier) - minerFee;
|
||||
if (outputAmount > bestOutput) {
|
||||
bestOutput = outputAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = bestOutput > 0 ? (bestOutput / amount) : 0;
|
||||
} else {
|
||||
double bestInput = double.infinity;
|
||||
for (var rate in rates) {
|
||||
final double rateMultiplier = double.parse(rate['amount']['value'].toString());
|
||||
final double minerFee = double.parse(rate['minerFee']['value'].toString());
|
||||
final double minVal = double.parse(rate['min']['value'].toString());
|
||||
final double maxVal = double.parse(rate['max']['value'].toString());
|
||||
final double requiredSend = (amount + minerFee) / rateMultiplier;
|
||||
if (requiredSend >= minVal && requiredSend <= maxVal) {
|
||||
if (requiredSend < bestInput) {
|
||||
bestInput = requiredSend;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = bestInput < double.infinity ? amount / bestInput : 0;
|
||||
}
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
responseData: {
|
||||
'result': result,
|
||||
'ratesCount': rates.length,
|
||||
'rates': rates,
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'fetchRate',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': from.title,
|
||||
'to': to.title,
|
||||
'amount': amount,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isReceiveAmount': isReceiveAmount,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> createTrade({
|
||||
required TradeRequest request,
|
||||
required bool isFixedRateMode,
|
||||
required bool isSendAll,
|
||||
}) async {
|
||||
try {
|
||||
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders');
|
||||
|
||||
final curFrom = await _getAssets(request.fromCurrency);
|
||||
final curTo = await _getAssets(request.toCurrency);
|
||||
|
||||
if (curFrom == null || curTo == null) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: TradeNotCreatedException(description),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'curFrom': curFrom,
|
||||
'curTo': curTo,
|
||||
},
|
||||
);
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
|
||||
final pairId = curFrom + '_' + curTo;
|
||||
|
||||
final payload = {
|
||||
'fromAmount': request.fromAmount,
|
||||
'fromAddress': request.refundAddress,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'pairId': pairId,
|
||||
};
|
||||
|
||||
final response = await ProxyWrapper().post(
|
||||
clearnetUri: uri,
|
||||
headers: _headers,
|
||||
body: json.encode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
final error = responseJSON['error'] ?? 'Unknown error';
|
||||
final message = responseJSON['message'] ?? '';
|
||||
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: Exception('$error\n$message'),
|
||||
stackTrace: StackTrace.current,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'payload': payload,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
throw Exception('$error\n$message');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final amount = responseJSON['amount'] as Map<String, dynamic>;
|
||||
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
|
||||
final orderId = responseJSON['id'] as String;
|
||||
final from = request.fromCurrency;
|
||||
final to = request.toCurrency;
|
||||
final payoutAddress = responseJSON['toAddress'] as String;
|
||||
final depositAddress = responseJSON['payInAddress'] as String;
|
||||
final refundAddress = responseJSON['fromAddress'] as String;
|
||||
final depositAmountStr = amount['value'].toString();
|
||||
final parsedAmount = double.tryParse(depositAmountStr);
|
||||
|
||||
if (parsedAmount == null || parsedAmount <= 0) {
|
||||
throw Exception('Invalid deposit amount received from API');
|
||||
}
|
||||
|
||||
final receiveAmount = toAmount['value'] as String;
|
||||
final status = responseJSON['status'] as String;
|
||||
final createdAtString = responseJSON['createdAt'] as String;
|
||||
final extraId = responseJSON['payInAddressTag'] as String?;
|
||||
|
||||
final createdAt = DateTime.parse(createdAtString).toLocal();
|
||||
|
||||
ExchangeProviderLogger.logSuccess(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
'payload': payload,
|
||||
'url': uri.toString(),
|
||||
},
|
||||
responseData: {
|
||||
'orderId': orderId,
|
||||
'depositAddress': depositAddress,
|
||||
'payoutAddress': payoutAddress,
|
||||
'refundAddress': refundAddress,
|
||||
'depositAmount': depositAmountStr,
|
||||
'receiveAmount': receiveAmount,
|
||||
'status': status,
|
||||
'createdAt': createdAtString,
|
||||
'extraId': extraId,
|
||||
'statusCode': response.statusCode,
|
||||
'responseJSON': responseJSON,
|
||||
},
|
||||
);
|
||||
|
||||
return Trade(
|
||||
id: orderId,
|
||||
from: from,
|
||||
to: to,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
refundAddress: refundAddress,
|
||||
state: TradeState.deserialize(raw: status),
|
||||
createdAt: createdAt,
|
||||
amount: depositAmountStr,
|
||||
receiveAmount: receiveAmount.toString(),
|
||||
payoutAddress: payoutAddress,
|
||||
extraId: extraId,
|
||||
isSendAll: isSendAll,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ExchangeProviderLogger.logError(
|
||||
provider: description,
|
||||
function: 'createTrade',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
requestData: {
|
||||
'from': request.fromCurrency.title,
|
||||
'to': request.toCurrency.title,
|
||||
'fromAmount': request.fromAmount,
|
||||
'toAmount': request.toAmount,
|
||||
'toAddress': request.toAddress,
|
||||
'refundAddress': request.refundAddress,
|
||||
'isFixedRateMode': isFixedRateMode,
|
||||
'isSendAll': isSendAll,
|
||||
},
|
||||
);
|
||||
printV(e.toString());
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Trade> findTradeById({required String id}) async {
|
||||
try {
|
||||
final uri = Uri.https(_apiAuthority, '$_apiPath$_orders/$id');
|
||||
final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
if (responseJSON.containsKey('code') && responseJSON['code'] == 'NOT_FOUND') {
|
||||
throw Exception('Trade not found');
|
||||
}
|
||||
final error = responseJSON['error'] ?? 'Unknown error';
|
||||
final message = responseJSON['message'] ?? responseJSON['details'] ?? '';
|
||||
throw Exception('$error\n$message');
|
||||
}
|
||||
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
|
||||
|
||||
final pairId = responseJSON['pairId'] as String;
|
||||
final pairParts = pairId.split('_');
|
||||
final fromAsset = pairParts.isNotEmpty ? pairParts[0] : '';
|
||||
final normalizedFromAsset = _normalizeXOSwapsNetwork(fromAsset);
|
||||
String? fromAssetTag = _extractTagFromAsset(normalizedFromAsset);
|
||||
|
||||
String fromAssetBase = fromAssetTag != null
|
||||
? normalizedFromAsset.substring(0, normalizedFromAsset.length - fromAssetTag.length)
|
||||
: normalizedFromAsset;
|
||||
|
||||
// Special case for USDT defaulting to ETH tag
|
||||
if (fromAssetBase == 'USDT' && fromAssetTag == null) {
|
||||
fromAssetTag = 'ETH';
|
||||
}
|
||||
|
||||
// Special case for BASE defaulting to BASE tag
|
||||
if (fromAssetBase == 'BASE' && fromAssetTag == null) {
|
||||
fromAssetTag = 'BASE';
|
||||
fromAssetBase = 'ETH';
|
||||
}
|
||||
|
||||
final toAsset = pairParts.length > 1 ? pairParts[1] : '';
|
||||
final normalizedToAsset = _normalizeXOSwapsNetwork(toAsset);
|
||||
String? toAssetTag = _extractTagFromAsset(normalizedToAsset);
|
||||
|
||||
String toAssetBase = toAssetTag != null
|
||||
? normalizedToAsset.substring(0, normalizedToAsset.length - toAssetTag.length)
|
||||
: normalizedToAsset;
|
||||
|
||||
// Special case for USDT defaulting to ETH tag
|
||||
if (toAssetBase == 'USDT' && toAssetTag == null) {
|
||||
toAssetTag = 'ETH';
|
||||
}
|
||||
|
||||
// Special case for BASE defaulting to BASE tag
|
||||
if (toAssetBase == 'BASE' && toAssetTag == null) {
|
||||
toAssetTag = 'ETH';
|
||||
toAssetBase = 'BASE';
|
||||
}
|
||||
|
||||
final fromCurrency = CryptoCurrency.safeParseCurrencyFromString(fromAssetBase,tag: fromAssetTag);
|
||||
final toCurrency = CryptoCurrency.safeParseCurrencyFromString(toAssetBase,tag: toAssetTag);
|
||||
|
||||
|
||||
final amount = responseJSON['amount'] as Map<String, dynamic>;
|
||||
final toAmount = responseJSON['toAmount'] as Map<String, dynamic>;
|
||||
final orderId = responseJSON['id'] as String;
|
||||
final depositAmountStr = amount['value'].toString();
|
||||
final parsedAmount = double.tryParse(depositAmountStr);
|
||||
|
||||
if (parsedAmount == null || parsedAmount <= 0) {
|
||||
throw Exception('Invalid deposit amount received from API');
|
||||
}
|
||||
|
||||
final receiveAmount = toAmount['value'] as String;
|
||||
final depositAddress = responseJSON['payInAddress'] as String;
|
||||
final payoutAddress = responseJSON['toAddress'] as String;
|
||||
final refundAddress = responseJSON['fromAddress'] as String;
|
||||
final status = responseJSON['status'] as String;
|
||||
final createdAtString = responseJSON['createdAt'] as String;
|
||||
final createdAt = DateTime.parse(createdAtString).toLocal();
|
||||
final extraId = responseJSON['payInAddressTag'] as String?;
|
||||
|
||||
return Trade(
|
||||
id: orderId,
|
||||
from: fromCurrency,
|
||||
to: toCurrency,
|
||||
provider: description,
|
||||
inputAddress: depositAddress,
|
||||
refundAddress: refundAddress,
|
||||
state: TradeState.deserialize(raw: status),
|
||||
createdAt: createdAt,
|
||||
amount: depositAmountStr,
|
||||
receiveAmount: receiveAmount,
|
||||
payoutAddress: payoutAddress,
|
||||
extraId: extraId,
|
||||
);
|
||||
} catch (e) {
|
||||
printV(e.toString());
|
||||
throw TradeNotCreatedException(description);
|
||||
}
|
||||
}
|
||||
|
||||
// ensure something remains before tag (at least 2 chars)
|
||||
String? _extractTagFromAsset(String asset) {
|
||||
|
||||
|
||||
|
||||
for (final tag in supportedTags) {
|
||||
if (asset.endsWith(tag)) {
|
||||
final prefixLength = asset.length - tag.length;
|
||||
if (prefixLength >= 2) {
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:hash_wallet/entities/qr_scanner.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_trade_state.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/modern_button.dart';
|
||||
@@ -281,14 +279,7 @@ class _NewSwapPageState extends State<NewSwapPage> {
|
||||
if (depositAmountController.text != widget.exchangeViewModel.depositAmount &&
|
||||
depositAmountController.text != S.of(context).all) {
|
||||
widget.exchangeViewModel.isSendAllEnabled = false;
|
||||
final isThorChain = widget.exchangeViewModel.selectedProviders
|
||||
.any((provider) => provider is ThorChainExchangeProvider);
|
||||
final isChainflip = widget.exchangeViewModel.selectedProviders
|
||||
.any((provider) => provider is ChainflipExchangeProvider);
|
||||
|
||||
_depositAmountDebounce = isThorChain || isChainflip
|
||||
? Debounce(Duration(milliseconds: 1000))
|
||||
: Debounce(Duration(milliseconds: 500));
|
||||
_depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
|
||||
_depositAmountDebounce.run(() {
|
||||
widget.exchangeViewModel.calculateBestRate();
|
||||
|
||||
@@ -32,7 +32,7 @@ class HistoryOrderTile extends StatelessWidget {
|
||||
date: date,
|
||||
amount: amount,
|
||||
amountFiat: amountFiat,
|
||||
leadingIcon:Image.asset("assets/images/cakepay.png"),
|
||||
leadingIcon: Image.asset("assets/images/card.png"),
|
||||
roundedTop: roundedTop,
|
||||
roundedBottom: roundedBottom,
|
||||
bottomSeparator: bottomSeparator,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/order/order.dart';
|
||||
import 'package:hash_wallet/order/order_provider.dart';
|
||||
import 'package:hash_wallet/order/order_provider_description.dart';
|
||||
import 'package:hash_wallet/order/order_source_description.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
|
||||
class CakePayOrderProviderAdapter implements OrderProvider {
|
||||
CakePayOrderProviderAdapter({
|
||||
required this.wallet,
|
||||
required this.cakePayService,
|
||||
});
|
||||
|
||||
final WalletBase wallet;
|
||||
final CakePayService cakePayService;
|
||||
|
||||
@override
|
||||
String get title => 'Cake Pay';
|
||||
|
||||
@override
|
||||
String get trackUrl => '';
|
||||
|
||||
@override
|
||||
Future<(Order, Object?)> findOrderById(String id) async {
|
||||
final cakePayOrder = await cakePayService.findOrderById(orderId: id);
|
||||
final cards = cakePayOrder.cards;
|
||||
final order = Order(
|
||||
id: cakePayOrder.orderId,
|
||||
state: determineState(cakePayOrder.status),
|
||||
transferId: '',
|
||||
createdAt: DateTime.now(),
|
||||
amount: '',
|
||||
receiveAmount: cakePayOrder.totalReceiveAmount,
|
||||
quantity: cakePayOrder.quantity.toString(),
|
||||
receiveAddress: '',
|
||||
walletId: wallet.id,
|
||||
from: '',
|
||||
to: cakePayOrder.fiatCurrencyCode,
|
||||
source: OrderSourceDescription.order,
|
||||
giftCardProvider: OrderProviderDescription.cakePay,
|
||||
);
|
||||
return (order, cards);
|
||||
}
|
||||
|
||||
TradeState determineState(String state) {
|
||||
final swapState = switch (state) {
|
||||
'new' => TradeState.pending,
|
||||
'expired_but_still_pending' => TradeState.overdue,
|
||||
'expired' => TradeState.expired,
|
||||
'failed' => TradeState.failed,
|
||||
'paid' => TradeState.paid,
|
||||
'paid_partial' => TradeState.underpaid,
|
||||
'pending_purchase' => TradeState.processing,
|
||||
'purchase_processing' => TradeState.processing,
|
||||
'purchased' => TradeState.confirmed,
|
||||
'pending_email' => TradeState.awaiting,
|
||||
'complete' => TradeState.complete,
|
||||
'pending_refund' => TradeState.refund,
|
||||
'refunded' => TradeState.refunded,
|
||||
_ => TradeState.notFound
|
||||
};
|
||||
|
||||
return swapState;
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import 'package:hash_wallet/src/screens/buy/buy_sell_options_page.dart';
|
||||
import 'package:hash_wallet/src/screens/buy/buy_webview_page.dart';
|
||||
import 'package:hash_wallet/src/screens/buy/payment_method_options_page.dart';
|
||||
import 'package:hash_wallet/src/screens/buy/webview_page.dart';
|
||||
import 'package:hash_wallet/cake_pay/cake_pay.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/monero_hardware_wallet_options_page.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/select_device_manufacturer_page.dart';
|
||||
@@ -778,37 +777,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
||||
param1: settings.arguments as QrViewData,
|
||||
));
|
||||
|
||||
case Routes.cakePayCardsPage:
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => getIt.get<CakePayCardsPage>(),
|
||||
);
|
||||
|
||||
case Routes.aboutPage:
|
||||
final page = getIt.get<AboutPage>();
|
||||
return handleRouteWithPlatformAwareness((context) => page);
|
||||
|
||||
case Routes.cakePayBuyCardPage:
|
||||
final args = settings.arguments as List;
|
||||
return handleRouteWithPlatformAwareness(
|
||||
(context) => getIt.get<CakePayBuyCardPage>(param1: args),
|
||||
);
|
||||
|
||||
case Routes.cakePayWelcomePage:
|
||||
return handleRouteWithPlatformAwareness<bool>(
|
||||
(context) => getIt.get<CakePayWelcomePage>(),
|
||||
);
|
||||
|
||||
case Routes.cakePayVerifyOtpPage:
|
||||
final args = settings.arguments as List;
|
||||
return handleRouteWithPlatformAwareness<bool>(
|
||||
(context) => getIt.get<CakePayVerifyOtpPage>(param1: args),
|
||||
);
|
||||
|
||||
case Routes.cakePayAccountPage:
|
||||
return handleRouteWithPlatformAwareness<bool>(
|
||||
(context) => getIt.get<CakePayAccountPage>(),
|
||||
);
|
||||
|
||||
case Routes.webViewPage:
|
||||
final args = settings.arguments as List;
|
||||
final title = args.first as String;
|
||||
|
||||
@@ -72,12 +72,6 @@ class Routes {
|
||||
static const unspentCoinsDetails = '/unspent_coins_details';
|
||||
static const addressPage = '/address_page';
|
||||
static const fullscreenQR = '/fullscreen_qr';
|
||||
static const cakePayWelcomePage = '/cake_pay_welcome_page';
|
||||
static const cakePayLoginPage = '/cake_pay_login_page';
|
||||
static const cakePayCardsPage = '/cake_pay_cards_page';
|
||||
static const cakePayBuyCardPage = '/cake_pay_buy_card_page';
|
||||
static const cakePayVerifyOtpPage = '/cake_pay_verify_otp_page';
|
||||
static const cakePayAccountPage = '/cake_pay_account_page';
|
||||
static const webViewPage = '/web_view_page';
|
||||
static const silentPaymentsSettings = '/silent_payments_settings';
|
||||
static const silentPaymentsLogs = '/silent_payments_logs';
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:hash_wallet/src/widgets/vulnerable_seeds_popup.dart';
|
||||
import 'package:hash_wallet/utils/device_info.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/version_comparator.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/yat_emoji_id.dart';
|
||||
@@ -327,7 +326,6 @@ class _DashboardPageView extends BasePage {
|
||||
label: S.of(context).apps,
|
||||
child: CakeFeaturesPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/main_actions.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
@@ -75,7 +73,6 @@ class DesktopDashboardActions extends StatelessWidget {
|
||||
Expanded(
|
||||
child: CakeFeaturesPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -7,10 +7,8 @@ import 'package:hash_wallet/src/screens/dashboard/widgets/info_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/introducing_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_switch.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
@@ -139,18 +137,6 @@ class CryptoBalanceWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
)),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) {
|
||||
return IntroducingCard(
|
||||
title: S.of(context).introducing_cake_pay,
|
||||
subTitle: S.of(context).cake_pay_learn_more,
|
||||
borderColor: Theme.of(context).colorScheme.outline,
|
||||
closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
Observer(builder: (_) {
|
||||
if (!dashboardViewModel.showRepWarning) {
|
||||
return const SizedBox();
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/apps_widget.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/cake_features_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
|
||||
class CakeFeaturesPage extends StatelessWidget {
|
||||
CakeFeaturesPage({required this.dashboardViewModel, required this.cakeFeaturesViewModel});
|
||||
CakeFeaturesPage({required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final CakeFeaturesViewModel cakeFeaturesViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -102,23 +95,4 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _onCakePayTap(BuildContext context) {
|
||||
if (Platform.isMacOS) {
|
||||
_launchUrl("buy.cakepay.com");
|
||||
} else {
|
||||
_navigatorToGiftCardsPage(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _launchUrl(String url) {
|
||||
try {
|
||||
launchUrl(Uri.https(url), mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigatorToGiftCardsPage(BuildContext context) {
|
||||
Navigator.pushNamed(context, Routes.cakePayCardsPage);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
@@ -542,14 +540,7 @@ class ExchangePage extends BasePage {
|
||||
if (depositAmountController.text != exchangeViewModel.depositAmount &&
|
||||
depositAmountController.text != S.of(context).all) {
|
||||
exchangeViewModel.isSendAllEnabled = false;
|
||||
final isThorChain = exchangeViewModel.selectedProviders
|
||||
.any((provider) => provider is ThorChainExchangeProvider);
|
||||
final isChainflip = exchangeViewModel.selectedProviders
|
||||
.any((provider) => provider is ChainflipExchangeProvider);
|
||||
|
||||
_depositAmountDebounce = isThorChain || isChainflip
|
||||
? Debounce(Duration(milliseconds: 1000))
|
||||
: Debounce(Duration(milliseconds: 500));
|
||||
_depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
|
||||
_depositAmountDebounce.run(() {
|
||||
exchangeViewModel.calculateBestRate();
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CakePayDetailsListCardItem extends StandartListItem {
|
||||
CakePayDetailsListCardItem({
|
||||
required String title,
|
||||
required String value,
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
this.price,
|
||||
this.quantity,
|
||||
required this.from,
|
||||
required this.to,
|
||||
required this.onTap,
|
||||
required this.cards,
|
||||
}) : super(title: title, value: value);
|
||||
|
||||
final String id;
|
||||
final String createdAt;
|
||||
final String? price;
|
||||
final String? quantity;
|
||||
final String from;
|
||||
final String to;
|
||||
final void Function(BuildContext) onTap;
|
||||
final List<OrderCard> cards;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import 'package:hash_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
|
||||
import 'package:hash_wallet/src/screens/trade_details/trade_details_status_item.dart';
|
||||
import 'package:hash_wallet/src/widgets/cake_pay_order_list_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_list_status_row.dart';
|
||||
import 'package:hash_wallet/themes/core/material_base_theme.dart';
|
||||
@@ -64,21 +62,6 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
|
||||
return StandardListStatusRow(title: item.title, value: item.value);
|
||||
}
|
||||
|
||||
if (item is CakePayDetailsListCardItem) {
|
||||
final cardImagePath = (item.cards.isNotEmpty) ? item.cards.first.cardImagePath : null;
|
||||
|
||||
return CakePayOrderListCard(
|
||||
id: item.id,
|
||||
create: item.createdAt,
|
||||
price: item.price,
|
||||
quantity: item.quantity,
|
||||
pair: '${item.from} → ${item.to}',
|
||||
currentTheme: widget.currentTheme.type,
|
||||
onTap: item.onTap,
|
||||
backgroundImage: cardImagePath,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TrackTradeListItem) {
|
||||
return GestureDetector(
|
||||
onTap: item.onTap,
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/cake_pay_alert_modal.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/flip_card_widget.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/widgets/link_extractor.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/utils/image_utill.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
|
||||
class CakePayCardInfoBottomSheet extends BaseBottomSheet {
|
||||
CakePayCardInfoBottomSheet({
|
||||
required String titleText,
|
||||
required FooterType footerType,
|
||||
String? titleIconPath,
|
||||
String? singleActionButtonText,
|
||||
VoidCallback? onSingleActionButtonPressed,
|
||||
Key? singleActionButtonKey,
|
||||
String? doubleActionLeftButtonText,
|
||||
String? doubleActionRightButtonText,
|
||||
VoidCallback? onLeftActionButtonPressed,
|
||||
VoidCallback? onRightActionButtonPressed,
|
||||
Key? rightActionButtonKey,
|
||||
Key? leftActionButtonKey,
|
||||
required this.onUpdateBalancePressed,
|
||||
required this.isReloadable,
|
||||
required this.balance,
|
||||
this.contentImage,
|
||||
this.howToUse,
|
||||
this.applyBoxShadow = false,
|
||||
Key? key,
|
||||
}) : super(
|
||||
titleText: titleText,
|
||||
maxHeight: 900,
|
||||
titleIconPath: titleIconPath,
|
||||
footerType: footerType,
|
||||
singleActionButtonText: singleActionButtonText,
|
||||
onSingleActionButtonPressed: onSingleActionButtonPressed,
|
||||
singleActionButtonKey: singleActionButtonKey,
|
||||
doubleActionLeftButtonText: doubleActionLeftButtonText,
|
||||
doubleActionRightButtonText: doubleActionRightButtonText,
|
||||
onLeftActionButtonPressed: onLeftActionButtonPressed,
|
||||
onRightActionButtonPressed: onRightActionButtonPressed,
|
||||
leftActionButtonKey: leftActionButtonKey,
|
||||
rightActionButtonKey: rightActionButtonKey,
|
||||
key: key);
|
||||
|
||||
final VoidCallback onUpdateBalancePressed;
|
||||
final String? contentImage;
|
||||
final String? howToUse;
|
||||
final bool isReloadable;
|
||||
final String balance;
|
||||
final bool applyBoxShadow;
|
||||
|
||||
final _cardKey = GlobalKey<FlipCardState>();
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
final itemTitleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
final itemSubTitleTextStyle = Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
|
||||
final tileBackgroundColor = Theme.of(context).colorScheme.surfaceContainer;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contentImage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 0, left: 16, right: 16, bottom: 16),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.6,
|
||||
child: FlipCard(
|
||||
key: _cardKey,
|
||||
flipOnTouch: true,
|
||||
front: _buildCardImage(context, contentImage!, applyBoxShadow),
|
||||
back: _buildBarcodeSide(context,
|
||||
cardNumber: '6006491979836784204', pin: '4782'),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: () => _cardKey.currentState?.toggleCard(),
|
||||
child: Container(
|
||||
height: 43,
|
||||
width: 43,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.white, width: 1),
|
||||
color: Colors.white.withAlpha(75),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: .8,
|
||||
child: const ImageIcon(
|
||||
AssetImage('assets/images/transfer.png'),
|
||||
color: Colors.white,
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(),
|
||||
Text(
|
||||
'Tap card to show details',
|
||||
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w700),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 34),
|
||||
CakePayInfoTile(
|
||||
isReloadable: isReloadable,
|
||||
itemValue: balance,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor),
|
||||
const SizedBox(height: 8),
|
||||
_HowToUseTile(howToUse: howToUse ?? '', tileBackgroundColor: tileBackgroundColor),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HowToUseTile extends StatelessWidget {
|
||||
const _HowToUseTile({required this.howToUse, required this.tileBackgroundColor});
|
||||
|
||||
final String howToUse;
|
||||
final Color tileBackgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => _showHowToUseCard(context: context, howToUse: howToUse),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration:
|
||||
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(S.of(context).how_to_use_card, style: Theme.of(context).textTheme.bodyLarge),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CakePayInfoTile extends StatelessWidget {
|
||||
const CakePayInfoTile({
|
||||
super.key,
|
||||
required this.isReloadable,
|
||||
required this.itemValue,
|
||||
required this.itemTitleTextStyle,
|
||||
this.itemSubTitle,
|
||||
required this.itemSubTitleTextStyle,
|
||||
required this.tileBackgroundColor,
|
||||
});
|
||||
|
||||
final bool isReloadable;
|
||||
final String itemValue;
|
||||
final TextStyle itemTitleTextStyle;
|
||||
final String? itemSubTitle;
|
||||
final TextStyle itemSubTitleTextStyle;
|
||||
final Color tileBackgroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: isReloadable ? 'Balance' : 'Total Value',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration:
|
||||
BoxDecoration(borderRadius: BorderRadius.circular(10), color: tileBackgroundColor),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(isReloadable ? 'Balance' : 'Total Value', style: itemTitleTextStyle),
|
||||
Text(itemValue,
|
||||
style: itemTitleTextStyle.copyWith(fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Text(isReloadable ? 'Top Up Balance' : 'Manually update Balance',
|
||||
style: itemTitleTextStyle.copyWith(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
color: Theme.of(context).dialogBackgroundColor)),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showHowToUseCard({required BuildContext context, String? howToUse}) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CakePayAlertModal(
|
||||
title: S.of(context).how_to_use_card,
|
||||
content: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
ClickableLinksText(
|
||||
text: howToUse ?? '',
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!,
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!,
|
||||
fontSize: 18,
|
||||
fontStyle: FontStyle.italic,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
]),
|
||||
actionTitle: S.current.got_it,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildCardImage(BuildContext ctx, String path, bool addShadow) {
|
||||
final border = BorderRadius.circular(10);
|
||||
|
||||
return Container(
|
||||
decoration: addShadow
|
||||
? BoxDecoration(
|
||||
borderRadius: border,
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)],
|
||||
)
|
||||
: null,
|
||||
child: ClipRRect(
|
||||
borderRadius: border,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(color: Theme.of(ctx).cardColor.withAlpha(200)),
|
||||
ImageUtil.getImageFromPath(
|
||||
imagePath: path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBarcodeSide(BuildContext context, {required String cardNumber, required String pin}) =>
|
||||
SizedBox.expand(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 34),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [BoxShadow(color: Colors.black.withAlpha(150), blurRadius: 5)],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Theme.of(context).textTheme.titleLarge!.color!.withOpacity(.1),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Gift Card Number',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color.fromRGBO(207, 207, 207, 1))),
|
||||
const SizedBox(height: 4),
|
||||
Text(cardNumber,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color.fromRGBO(146, 146, 146, 1))),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('PIN Number',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color.fromRGBO(207, 207, 207, 1))),
|
||||
const SizedBox(height: 4),
|
||||
Text(pin,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Lato',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Color.fromRGBO(146, 146, 146, 1))),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1,315 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/rounded_icon_button.dart';
|
||||
import 'package:hash_wallet/utils/image_utill.dart';
|
||||
import 'package:hash_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CakePayTransactionSentBottomSheet extends StatelessWidget {
|
||||
const CakePayTransactionSentBottomSheet({
|
||||
super.key,
|
||||
required this.titleText,
|
||||
required this.currency,
|
||||
required this.amount,
|
||||
required this.amountValue,
|
||||
required this.fiatAmountValue,
|
||||
required this.output,
|
||||
required this.fee,
|
||||
required this.feeValue,
|
||||
required this.feeFiatAmount,
|
||||
this.titleIconWidget,
|
||||
required this.quantity,
|
||||
required this.onClose,
|
||||
required this.paymentId,
|
||||
required this.paymentIdValue,
|
||||
});
|
||||
|
||||
final String titleText;
|
||||
final Widget? titleIconWidget;
|
||||
final CryptoCurrency currency;
|
||||
final String amount;
|
||||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
final Output output;
|
||||
final String fee;
|
||||
final String feeValue;
|
||||
final String feeFiatAmount;
|
||||
final String quantity;
|
||||
final VoidCallback onClose;
|
||||
final String paymentId;
|
||||
final String paymentIdValue;
|
||||
|
||||
TextStyle _titleStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.bodyLarge!;
|
||||
|
||||
TextStyle _valueStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
|
||||
TextStyle _subStyle(BuildContext ctx) =>
|
||||
Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.copyWith(
|
||||
color: Theme
|
||||
.of(ctx)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
);
|
||||
|
||||
Widget _buildHeader(BuildContext ctx) =>
|
||||
Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Theme
|
||||
.of(ctx)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (titleIconWidget != null) titleIconWidget!,
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
titleText,
|
||||
style: Theme
|
||||
.of(ctx)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildBody(BuildContext context) =>
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
_StandardTile(
|
||||
itemTitle: amount,
|
||||
titleStyle: _titleStyle(context),
|
||||
itemValue: '$amountValue ${currency.title}',
|
||||
itemValueStyle: _valueStyle(context),
|
||||
itemSubTitle: fiatAmountValue,
|
||||
itemSubTitleStyle: _subStyle(context),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_StandardTile(
|
||||
itemTitle: fee,
|
||||
titleStyle: _titleStyle(context),
|
||||
itemValue: feeValue,
|
||||
itemValueStyle: _valueStyle(context),
|
||||
itemSubTitle: feeFiatAmount,
|
||||
itemSubTitleStyle: _subStyle(context),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_StandardTile(
|
||||
itemTitle: output.parsedAddress.profileName,
|
||||
titleStyle: _titleStyle(context),
|
||||
itemValue: output.fiatAmount,
|
||||
itemValueStyle: _valueStyle(context),
|
||||
itemSubTitle: quantity,
|
||||
itemSubTitleStyle: _subStyle(context),
|
||||
imagePath: output.parsedAddress.profileImageUrl,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_StandardTile(
|
||||
itemTitle: paymentId + ': \n' + paymentIdValue,
|
||||
titleStyle: _titleStyle(context).copyWith(
|
||||
fontSize: 14
|
||||
),
|
||||
itemValue: '',
|
||||
itemValueStyle: _valueStyle(context),
|
||||
copyButton: true,
|
||||
copyButtonOnPressed: () async =>
|
||||
await Clipboard.setData(ClipboardData(text: paymentIdValue)),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 18),
|
||||
Image.asset('assets/images/envelope.png'),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
S
|
||||
.of(context)
|
||||
.cake_pay_card_email_delivered_message,
|
||||
style: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
PrimaryButton(
|
||||
text: S
|
||||
.of(context)
|
||||
.close,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
textColor: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer,
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final maxHeight = MediaQuery
|
||||
.of(context)
|
||||
.size
|
||||
.height * 0.9;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
child: Material(
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.surface,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
_buildBody(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StandardTile extends StatelessWidget {
|
||||
const _StandardTile({
|
||||
required this.itemTitle,
|
||||
required this.titleStyle,
|
||||
required this.itemValue,
|
||||
required this.itemValueStyle,
|
||||
this.itemSubTitle,
|
||||
this.itemSubTitleStyle,
|
||||
this.imagePath,
|
||||
this.copyButton = false,
|
||||
this.copyButtonOnPressed,
|
||||
});
|
||||
|
||||
final String itemTitle;
|
||||
final TextStyle titleStyle;
|
||||
final String itemValue;
|
||||
final TextStyle itemValueStyle;
|
||||
final String? itemSubTitle;
|
||||
final TextStyle? itemSubTitleStyle;
|
||||
final String? imagePath;
|
||||
final bool copyButton;
|
||||
final VoidCallback? copyButtonOnPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: itemTitle,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLowest
|
||||
.withAlpha(80)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: copyButton ? MainAxisAlignment.spaceBetween : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (imagePath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: ImageUtil.getImageFromPath(
|
||||
imagePath: imagePath!, height: 40, width: 40),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(
|
||||
itemTitle,
|
||||
style: titleStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
if (copyButton)
|
||||
RoundedIconButton(
|
||||
icon: Icons.copy_all_outlined,
|
||||
onPressed: copyButtonOnPressed ?? () {},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(itemValue, style: itemValueStyle.copyWith(height: 1.0)),
|
||||
if (itemSubTitle != null) Text(itemSubTitle!, style: itemSubTitleStyle),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,12 @@ import 'package:hash_wallet/src/widgets/rounded_icon_button.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:hash_wallet/utils/address_formatter.dart';
|
||||
import 'package:hash_wallet/utils/image_utill.dart';
|
||||
import 'package:hash_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
import 'base_bottom_sheet_widget.dart';
|
||||
|
||||
@@ -26,9 +24,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
bool isSlideActionEnabled = true,
|
||||
String? accessibleNavigationModeSlideActionButtonText,
|
||||
required this.currency,
|
||||
this.paymentId,
|
||||
this.paymentIdValue,
|
||||
this.expirationTime,
|
||||
required this.amount,
|
||||
required this.amountValue,
|
||||
required this.fiatAmountValue,
|
||||
@@ -41,8 +36,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
this.change,
|
||||
this.explanation,
|
||||
this.isOpenCryptoPay = false,
|
||||
this.cakePayBuyCardViewModel,
|
||||
this.quantity,
|
||||
Key? key,
|
||||
}) : showScrollbar = outputs.length > 3,
|
||||
super(
|
||||
@@ -58,9 +51,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
key: key);
|
||||
|
||||
final CryptoCurrency currency;
|
||||
final String? paymentId;
|
||||
final String? paymentIdValue;
|
||||
final String? expirationTime;
|
||||
final String amount;
|
||||
final String amountValue;
|
||||
final String fiatAmountValue;
|
||||
@@ -71,8 +61,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
final WalletType walletType;
|
||||
final PendingChange? change;
|
||||
final bool isOpenCryptoPay;
|
||||
final CakePayBuyCardViewModel? cakePayBuyCardViewModel;
|
||||
final String? quantity;
|
||||
final String? explanation;
|
||||
final AmountParsingProxy? amountParsingProxy;
|
||||
|
||||
@@ -103,26 +91,6 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8),
|
||||
child: Column(
|
||||
children: [
|
||||
if (paymentId != null && paymentIdValue != null && cakePayBuyCardViewModel != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Observer(
|
||||
builder: (_) => AddressTile(
|
||||
itemTitle: paymentId!,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
amountTextStyle: itemSubTitleTextStyle,
|
||||
walletType: walletType,
|
||||
amount: expirationTime != null
|
||||
? S.current.offer_expires_in +
|
||||
' ${cakePayBuyCardViewModel!.formattedRemainingTime}'
|
||||
: null,
|
||||
address: paymentIdValue!,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
applyAddressFormatting: false,
|
||||
copyButton: true,
|
||||
)),
|
||||
),
|
||||
if (explanation != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
@@ -163,12 +131,11 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
final isBatchSending = outputs.length > 1;
|
||||
final item = outputs[index];
|
||||
final contactName = item.parsedAddress.name;
|
||||
final isCakePayName = contactName == 'Cake Pay';
|
||||
final batchContactTitle =
|
||||
'${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}';
|
||||
final _address = item.isParsedAddress ? item.extractedAddress : item.address;
|
||||
final _amount = '${item.cryptoAmount.replaceAll(',', '.')} ${amountParsingProxy?.getCryptoSymbol(currency) ?? currency.title}';
|
||||
return isBatchSending || (contactName.isNotEmpty && !isCakePayName)
|
||||
return isBatchSending || contactName.isNotEmpty
|
||||
? ExpansionAddressTile(
|
||||
contactType: isOpenCryptoPay ? 'Open CryptoPay' : S.of(context).contact,
|
||||
name: isBatchSending ? batchContactTitle : contactName,
|
||||
@@ -181,15 +148,11 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
)
|
||||
: AddressTile(
|
||||
itemTitle: isCakePayName
|
||||
? item.parsedAddress.profileName
|
||||
: S.of(context).address,
|
||||
imagePath: isCakePayName ? item.parsedAddress.profileImageUrl : null,
|
||||
itemTitle: S.of(context).address,
|
||||
itemTitleTextStyle: itemTitleTextStyle,
|
||||
walletType: walletType,
|
||||
amount: isCakePayName ? item.fiatAmount : _amount,
|
||||
amount: _amount,
|
||||
address: _address,
|
||||
itemSubTitle: isCakePayName ? quantity : null,
|
||||
itemSubTitleTextStyle: itemSubTitleTextStyle,
|
||||
tileBackgroundColor: tileBackgroundColor,
|
||||
);
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/themes/core/material_base_theme.dart';
|
||||
|
||||
class CakePayOrderListCard extends StatelessWidget {
|
||||
const CakePayOrderListCard({
|
||||
Key? key,
|
||||
required this.id,
|
||||
required this.create,
|
||||
this.quantity,
|
||||
this.price,
|
||||
required this.pair,
|
||||
required this.onTap,
|
||||
required this.currentTheme,
|
||||
this.backgroundImage,
|
||||
this.aspectRatio = 16 / 9,
|
||||
}) : super(key: key);
|
||||
|
||||
final String id;
|
||||
final String create;
|
||||
final String? quantity;
|
||||
final String? price;
|
||||
final String pair;
|
||||
final ThemeType currentTheme;
|
||||
final void Function(BuildContext) onTap;
|
||||
final String? backgroundImage;
|
||||
final double aspectRatio;
|
||||
|
||||
ImageProvider? _imageProviderFor(String? path) {
|
||||
if (path == null || path.isEmpty) return null;
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) return NetworkImage(path);
|
||||
return AssetImage(path);
|
||||
}
|
||||
|
||||
Color _labelBackground(BuildContext context) {
|
||||
final surface = Theme.of(context).colorScheme.surface;
|
||||
return surface.withOpacity(0.75);
|
||||
}
|
||||
|
||||
Color _titleLabelBackground(BuildContext context) {
|
||||
final surface = Theme.of(context).colorScheme.surface;
|
||||
return surface.withOpacity(0.85);
|
||||
}
|
||||
|
||||
Widget _labelBox(
|
||||
BuildContext context, {
|
||||
required String text,
|
||||
EdgeInsets padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
double radius = 10,
|
||||
Color? background,
|
||||
TextStyle? style,
|
||||
}) {
|
||||
return Container(
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: background ?? _labelBackground(context),
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: style ??
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final image = _imageProviderFor(backgroundImage);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => onTap(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (image != null)
|
||||
Image(image: image, fit: BoxFit.cover)
|
||||
else
|
||||
Container(color: Theme.of(context).colorScheme.surfaceContainerHighest),
|
||||
if (image != null)
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
stops: [0.0, 0.6, 1.0],
|
||||
colors: [Colors.transparent, Colors.black26, Colors.black54],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_labelBox(
|
||||
context,
|
||||
text: 'Create • $create',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
radius: 8,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_labelBox(
|
||||
context,
|
||||
text: 'Qty • $quantity Total • $price',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
radius: 8,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_labelBox(
|
||||
context,
|
||||
text: pair,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
radius: 12,
|
||||
background: _titleLabelBackground(context),
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_labelBox(
|
||||
context,
|
||||
text: 'Order • $id',
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
radius: 10,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(currentTheme == ThemeType.dark ? 0.08 : 0.12),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,6 @@ import 'package:cw_core/utils/tor/disabled.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FeatureFlag {
|
||||
static const bool isCakePayEnabled = false;
|
||||
static const bool isCakePayPurchaseSimulationEnabled = true;
|
||||
static const bool isCakePayRedemptionFlowEnabled = false;
|
||||
static const bool isExolixEnabled = true;
|
||||
// Hash Bags: Lightning disabled — running Greenlight/LSP infra is out of
|
||||
// scope for now. Re-enable once we have an LSP relationship lined up.
|
||||
static const bool isLightningEnabled = false;
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:hash_wallet/buy/buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/buy_quote.dart';
|
||||
import 'package:hash_wallet/buy/onramper/onramper_buy_provider.dart';
|
||||
import 'package:hash_wallet/buy/payment_method.dart';
|
||||
import 'package:hash_wallet/buy/sell_buy_states.dart';
|
||||
import 'package:hash_wallet/core/selectable_option.dart';
|
||||
@@ -455,18 +454,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
final List<Quote> successRateQuotes = validQuotes
|
||||
.where((element) =>
|
||||
element.provider is OnRamperBuyProvider &&
|
||||
element.recommendations.contains(ProviderRecommendation.successRate))
|
||||
.toList();
|
||||
|
||||
for (final quote in successRateQuotes) {
|
||||
if (!uniqueProviderQuotes.contains(quote)) {
|
||||
uniqueProviderQuotes.add(quote);
|
||||
}
|
||||
}
|
||||
|
||||
sortedRecommendedQuotes.addAll(uniqueProviderQuotes);
|
||||
|
||||
sortedQuotes = ObservableList.of(
|
||||
@@ -477,12 +464,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
|
||||
..setIsBestRate = true;
|
||||
bestRateQuote = sortedRecommendedQuotes.first;
|
||||
|
||||
sortedRecommendedQuotes.sort((a, b) {
|
||||
if (a.provider is OnRamperBuyProvider) return -1;
|
||||
if (b.provider is OnRamperBuyProvider) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
final Quote effectiveBestRateQuote =
|
||||
sortedRecommendedQuotes.reduce((a, b) {
|
||||
return isBuyAction ? a.rate < b.rate ? a : b : a.rate > b.rate ? a : b;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_account_view_model.g.dart';
|
||||
|
||||
class CakePayAccountViewModel = CakePayAccountViewModelBase with _$CakePayAccountViewModel;
|
||||
|
||||
abstract class CakePayAccountViewModelBase with Store {
|
||||
CakePayAccountViewModelBase({required this.cakePayService}) : email = '' {
|
||||
cakePayService.getUserEmail().then((email) => this.email = email ?? '');
|
||||
}
|
||||
|
||||
final CakePayService cakePayService;
|
||||
|
||||
@observable
|
||||
String email;
|
||||
|
||||
@action
|
||||
Future<void> logout() async => cakePayService.logout(email);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_auth_view_model.g.dart';
|
||||
|
||||
class CakePayAuthViewModel = CakePayAuthViewModelBase with _$CakePayAuthViewModel;
|
||||
|
||||
abstract class CakePayAuthViewModelBase with Store {
|
||||
CakePayAuthViewModelBase({required this.cakePayService})
|
||||
: userVerificationState = CakePayUserVerificationStateInitial(),
|
||||
otpState = CakePayOtpSendDisabled(),
|
||||
email = '',
|
||||
otp = '';
|
||||
|
||||
final CakePayService cakePayService;
|
||||
|
||||
@observable
|
||||
CakePayUserVerificationState userVerificationState;
|
||||
|
||||
@observable
|
||||
CakePayOtpState otpState;
|
||||
|
||||
@observable
|
||||
String email;
|
||||
|
||||
@observable
|
||||
String otp;
|
||||
|
||||
@action
|
||||
Future<void> verifyEmail(String code) async {
|
||||
try {
|
||||
otpState = CakePayOtpValidating();
|
||||
await cakePayService.verifyEmail(code);
|
||||
otpState = CakePayOtpSuccess();
|
||||
} catch (_) {
|
||||
otpState = CakePayOtpFailure(error: 'Invalid OTP. Try again');
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> logIn(String email) async {
|
||||
try {
|
||||
userVerificationState = CakePayUserVerificationStateLoading();
|
||||
await cakePayService.logIn(email);
|
||||
userVerificationState = CakePayUserVerificationStateSuccess();
|
||||
} catch (e) {
|
||||
userVerificationState = CakePayUserVerificationStateFailure(error: e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hash_wallet/order/order_provider_description.dart';
|
||||
import 'package:hash_wallet/order/order.dart';
|
||||
import 'package:hash_wallet/order/order_source_description.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/exchange/trade_state.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_buy_card_view_model.g.dart';
|
||||
|
||||
class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCardViewModel;
|
||||
|
||||
abstract class CakePayBuyCardViewModelBase with Store {
|
||||
CakePayBuyCardViewModelBase(
|
||||
{required this.vendor,
|
||||
required CakePayService cakePayService,
|
||||
required this.sendViewModel,
|
||||
required this.orders})
|
||||
: _cakePayService = cakePayService,
|
||||
walletType = sendViewModel.walletType,
|
||||
amount = vendor.card!.denominationItems.isNotEmpty
|
||||
? vendor.card!.denominationItems.first.value
|
||||
: 0,
|
||||
quantity = 1,
|
||||
card = vendor.card!,
|
||||
min = _toDouble(vendor.card!.minValue) ?? 0,
|
||||
max = _toDouble(vendor.card!.maxValue) ?? 0 {
|
||||
selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null;
|
||||
}
|
||||
|
||||
static double? _toDouble(String? value) {
|
||||
if (value == null || value.isEmpty) return null;
|
||||
return double.tryParse(value.replaceAll(',', '.'));
|
||||
}
|
||||
|
||||
final CakePayVendor vendor;
|
||||
final SendViewModel sendViewModel;
|
||||
final CakePayService _cakePayService;
|
||||
final double max;
|
||||
final double min;
|
||||
final CakePayCard card;
|
||||
final WalletType walletType;
|
||||
final Box<Order> orders;
|
||||
|
||||
CakePayOrder? order;
|
||||
Timer? _timer;
|
||||
DateTime? expirationTime;
|
||||
Duration? remainingTime;
|
||||
bool confirmsNoVpn = false;
|
||||
bool confirmsVoidedRefund = false;
|
||||
bool confirmsTermsAgreed = false;
|
||||
(String, int?) selectedDenomination = ('', null);
|
||||
|
||||
String simulatedResponse = '';
|
||||
|
||||
bool get isDenominationSelected =>
|
||||
card.denominationItems.isNotEmpty &&
|
||||
card.denominationItems.any((item) => item.value == amount);
|
||||
|
||||
Future<bool> get isUserLogged async => await _cakePayService.isLogged();
|
||||
|
||||
@observable
|
||||
double amount;
|
||||
|
||||
@observable
|
||||
int quantity;
|
||||
|
||||
@observable
|
||||
bool isPurchasing = false;
|
||||
|
||||
@observable
|
||||
bool isSimulatingFlow = false;
|
||||
|
||||
@observable
|
||||
bool isOrderExpired = false;
|
||||
|
||||
@observable
|
||||
String formattedRemainingTime = '';
|
||||
|
||||
@computed
|
||||
bool get isAmountSufficient =>
|
||||
(amount >= min && amount <= max) || (isDenominationSelected && quantity > 0);
|
||||
|
||||
@observable
|
||||
CakePayPaymentMethod? selectedPaymentMethod;
|
||||
|
||||
@computed
|
||||
double get totalAmount => amount * quantity;
|
||||
|
||||
@computed
|
||||
bool get isSimulating =>
|
||||
isSimulatingFlow &&
|
||||
FeatureFlag.hasDevOptions &&
|
||||
FeatureFlag.isCakePayPurchaseSimulationEnabled;
|
||||
|
||||
@computed
|
||||
List<CakePayPaymentMethod> get availableMethods {
|
||||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
return [
|
||||
CakePayPaymentMethod.BTC,
|
||||
if (sendViewModel.wallet.isSoftwareWallet) CakePayPaymentMethod.BTC_LN
|
||||
];
|
||||
case WalletType.litecoin:
|
||||
return [
|
||||
CakePayPaymentMethod.LTC,
|
||||
if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB
|
||||
];
|
||||
case WalletType.monero:
|
||||
return [CakePayPaymentMethod.XMR];
|
||||
default:
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void chooseMethod(CakePayPaymentMethod method) {
|
||||
selectedPaymentMethod = method;
|
||||
if (walletType == WalletType.litecoin) {
|
||||
sendViewModel.setAllowMwebCoins(method == CakePayPaymentMethod.LTC_MWEB);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void onQuantityChanged(int? input) => quantity = input ?? 1;
|
||||
|
||||
@action
|
||||
void onAmountChanged(String input) {
|
||||
if (input.isEmpty) return;
|
||||
amount = double.parse(input.replaceAll(',', '.'));
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> createOrder() async {
|
||||
if (![
|
||||
WalletType.bitcoin,
|
||||
WalletType.monero,
|
||||
WalletType.litecoin,
|
||||
].contains(walletType)) {
|
||||
sendViewModel.state =
|
||||
FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.');
|
||||
}
|
||||
try {
|
||||
order = await _cakePayService.createOrder(
|
||||
cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id,
|
||||
price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(),
|
||||
quantity: quantity,
|
||||
confirmsNoVpn: confirmsNoVpn,
|
||||
confirmsVoidedRefund: confirmsVoidedRefund,
|
||||
confirmsTermsAgreed: confirmsTermsAgreed,
|
||||
);
|
||||
final paymentData =
|
||||
CakePayOrder.getPaymentDataFor(method: selectedPaymentMethod, order: order); // TODO should hande other currencies if added
|
||||
if (paymentData == null || order == null)
|
||||
throw Exception('Payment data or order is not available.');
|
||||
|
||||
await confirmSending(paymentData);
|
||||
expirationTime = order!.paymentData.expirationTime;
|
||||
|
||||
final orderRecord = Order(
|
||||
id: order!.orderId,
|
||||
state: TradeState.deserialize(raw: order!.status),
|
||||
transferId: order!.externalId ?? '',
|
||||
from: CakePayOrder.getCurrencyCodeFromPaymentMethod(selectedPaymentMethod!),
|
||||
to: order!.fiatCurrencyCode,
|
||||
createdAt: DateTime.now(),
|
||||
amount: paymentData.amount,
|
||||
receiveAmount: order!.totalReceiveAmount,
|
||||
quantity: order!.quantity.toString(),
|
||||
receiveAddress: paymentData.address,
|
||||
source: OrderSourceDescription.order,
|
||||
giftCardProvider: OrderProviderDescription.cakePay,
|
||||
walletId: sendViewModel.wallet.id);
|
||||
orders.add(orderRecord);
|
||||
updateRemainingTime();
|
||||
_startExpirationTimer();
|
||||
} catch (e) {
|
||||
sendViewModel.state = FailureState(
|
||||
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> confirmSending(CryptoPaymentData paymentData) async {
|
||||
try {
|
||||
sendViewModel.clearOutputs();
|
||||
final output = sendViewModel.outputs.first;
|
||||
output.address = paymentData.address;
|
||||
output.setCryptoAmount(paymentData.price);
|
||||
|
||||
await sendViewModel.createTransaction();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> simulatePayment() async {
|
||||
if (order == null) {
|
||||
throw Exception('Order is not created yet.');
|
||||
}
|
||||
|
||||
try {
|
||||
simulatedResponse = await _cakePayService.simulatePayment(orderId: order!.orderId);
|
||||
sendViewModel.state = TransactionCommitted();
|
||||
} catch (e) {
|
||||
sendViewModel.state = FailureState(
|
||||
sendViewModel.translateErrorMessage(e, walletType, sendViewModel.wallet.currency));
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void updateRemainingTime() {
|
||||
if (expirationTime == null) {
|
||||
formattedRemainingTime = '';
|
||||
return;
|
||||
}
|
||||
|
||||
remainingTime = expirationTime!.difference(DateTime.now());
|
||||
|
||||
isOrderExpired = remainingTime!.isNegative;
|
||||
|
||||
if (isOrderExpired) {
|
||||
disposeExpirationTimer();
|
||||
sendViewModel.state = FailureState('Order has expired.');
|
||||
} else {
|
||||
formattedRemainingTime = formatDuration(remainingTime!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async => await _cakePayService.logout();
|
||||
|
||||
void _startExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
||||
updateRemainingTime();
|
||||
});
|
||||
}
|
||||
|
||||
String formatDuration(Duration duration) {
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void disposeExpirationTimer() {
|
||||
_timer?.cancel();
|
||||
remainingTime = null;
|
||||
formattedRemainingTime = '';
|
||||
expirationTime = null;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disposeExpirationTimer();
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_card.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/cake_pay_states.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_vendor.dart';
|
||||
import 'package:hash_wallet/entities/country.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/filter_item.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_pay_cards_list_view_model.g.dart';
|
||||
|
||||
class CakePayCardsListViewModel = CakePayCardsListViewModelBase with _$CakePayCardsListViewModel;
|
||||
|
||||
abstract class CakePayCardsListViewModelBase with Store {
|
||||
CakePayCardsListViewModelBase({
|
||||
required this.cakePayService,
|
||||
required this.settingsStore,
|
||||
}) : cakePayVendors = [],
|
||||
userCards = [],
|
||||
availableCountries = ObservableList<Country>.of(Country.allForCakePay),
|
||||
page = 1,
|
||||
displayPrepaidCards = true,
|
||||
displayGiftCards = true,
|
||||
displayDenominationsCards = true,
|
||||
displayCustomValueCards = true,
|
||||
scrollOffsetFromTop = 0.0,
|
||||
vendorsState = InitialCakePayVendorLoadingState(),
|
||||
createCardState = CakePayCreateCardState(),
|
||||
userCardState = UserCakePayCardsStateInitial(),
|
||||
searchString = '',
|
||||
searchMyCardsString = '' {
|
||||
checkAuth();
|
||||
initialization();
|
||||
}
|
||||
|
||||
static Country _getInitialCountry(FiatCurrency fiatCurrency) {
|
||||
if (fiatCurrency.countryCode == 'eur') {
|
||||
return Country.deu;
|
||||
}
|
||||
return Country.fromCode(fiatCurrency.countryCode) ?? Country.usa;
|
||||
}
|
||||
|
||||
void initialization() async {
|
||||
getVendors();
|
||||
getUserCards();
|
||||
}
|
||||
|
||||
final CakePayService cakePayService;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
Map<String, List<FilterItem>> get createFilterItems => {
|
||||
'Card Type': [
|
||||
FilterItem(
|
||||
value: () => displayPrepaidCards,
|
||||
caption: S.current.prepaid_cards,
|
||||
onChanged: togglePrepaidCards),
|
||||
FilterItem(
|
||||
value: () => displayGiftCards,
|
||||
caption: S.current.gift_cards,
|
||||
onChanged: toggleGiftCards),
|
||||
],
|
||||
// Uncomment if will be added to backend
|
||||
// S.current.value_type: [
|
||||
// FilterItem(
|
||||
// value: () => displayDenominationsCards,
|
||||
// caption: S.current.denominations,
|
||||
// onChanged: toggleDenominationsCards),
|
||||
// FilterItem(
|
||||
// value: () => displayCustomValueCards,
|
||||
// caption: S.current.custom_value,
|
||||
// onChanged: toggleCustomValueCards),
|
||||
// ],
|
||||
};
|
||||
|
||||
String searchString;
|
||||
String? username;
|
||||
int page;
|
||||
|
||||
late Country _initialSelectedCountry;
|
||||
late bool _initialDisplayPrepaidCards;
|
||||
late bool _initialDisplayGiftCards;
|
||||
late bool _initialDisplayDenominationsCards;
|
||||
late bool _initialDisplayCustomValueCards;
|
||||
|
||||
@observable
|
||||
List<CakePayCard> userCards;
|
||||
|
||||
@observable
|
||||
String searchMyCardsString;
|
||||
|
||||
@observable
|
||||
double scrollOffsetFromTop;
|
||||
|
||||
@observable
|
||||
CakePayCreateCardState createCardState;
|
||||
|
||||
@observable
|
||||
UserCakePayCardsState userCardState;
|
||||
|
||||
@observable
|
||||
CakePayVendorState vendorsState;
|
||||
|
||||
@observable
|
||||
bool hasMoreDataToFetch = true;
|
||||
|
||||
@observable
|
||||
bool isLoadingNextPage = false;
|
||||
|
||||
@observable
|
||||
List<CakePayVendor> cakePayVendors;
|
||||
|
||||
@observable
|
||||
List<Country> availableCountries;
|
||||
|
||||
@observable
|
||||
bool displayPrepaidCards;
|
||||
|
||||
@observable
|
||||
bool displayGiftCards;
|
||||
|
||||
@observable
|
||||
bool displayDenominationsCards;
|
||||
|
||||
@observable
|
||||
bool displayCustomValueCards;
|
||||
|
||||
@observable
|
||||
ObservableFuture<bool>? authFuture;
|
||||
|
||||
@computed
|
||||
bool? get isUserAuthenticated =>
|
||||
authFuture?.status == FutureStatus.fulfilled ? authFuture?.value : null;
|
||||
|
||||
@computed
|
||||
Country get selectedCountry =>
|
||||
settingsStore.selectedCakePayCountry ?? _getInitialCountry(settingsStore.fiatCurrency);
|
||||
|
||||
@computed
|
||||
bool get shouldShowCountryPicker =>
|
||||
settingsStore.selectedCakePayCountry == null && availableCountries.isNotEmpty;
|
||||
|
||||
@computed
|
||||
List<CakePayCard> get filteredUserCards {
|
||||
final query = searchMyCardsString.trim().toLowerCase();
|
||||
if (query.isEmpty) return userCards;
|
||||
return userCards
|
||||
.where((card) => card.name.toLowerCase().contains(query))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
|
||||
bool get hasFiltersChanged {
|
||||
return selectedCountry != _initialSelectedCountry ||
|
||||
displayPrepaidCards != _initialDisplayPrepaidCards ||
|
||||
displayGiftCards != _initialDisplayGiftCards ||
|
||||
displayDenominationsCards != _initialDisplayDenominationsCards ||
|
||||
displayCustomValueCards != _initialDisplayCustomValueCards;
|
||||
}
|
||||
|
||||
Future<void> getUserCards() async {
|
||||
//Dummy user cards // TODO: fetch from API
|
||||
userCardState = UserCakePayCardsStateFetching();
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
final vendorsCard = cakePayVendors
|
||||
.where((vendor) => vendor.card != null)
|
||||
.map((vendor) => vendor.card!)
|
||||
.toList();
|
||||
userCards = vendorsCard.sublist(0, 10);
|
||||
|
||||
vendorsCard.forEach((card) {
|
||||
if (card.name.toLowerCase().contains('amazon.com')) {
|
||||
userCards.add(card);
|
||||
}
|
||||
});
|
||||
|
||||
userCardState = UserCakePayCardsStateSuccess();
|
||||
if (userCards.isEmpty) {
|
||||
userCardState = UserCakePayCardsStateNoCards();
|
||||
}
|
||||
} catch (e) {
|
||||
userCardState = UserCakePayCardsStateFailure(error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> checkAuth() async {
|
||||
authFuture = ObservableFuture(cakePayService.isLogged());
|
||||
|
||||
final logged = await authFuture!;
|
||||
if (logged) {
|
||||
username = await cakePayService.getUserEmail();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> getVendors({
|
||||
String? text,
|
||||
int? currentPage,
|
||||
}) async {
|
||||
vendorsState = CakePayVendorLoadingState();
|
||||
try {
|
||||
searchString = text ?? '';
|
||||
var newVendors = await cakePayService.getVendors(
|
||||
countryCode: selectedCountry.countryCode,
|
||||
page: currentPage ?? page,
|
||||
search: searchString,
|
||||
giftCards: displayGiftCards,
|
||||
prepaidCards: displayPrepaidCards);
|
||||
|
||||
cakePayVendors = newVendors.where((vendor) {
|
||||
if (vendor.card == null) return false;
|
||||
if (vendor.available != true) return false;
|
||||
return true;
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
vendorsState = CakePayVendorLoadedState();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> fetchNextPage() async {
|
||||
if (vendorsState is CakePayVendorLoadingState || !hasMoreDataToFetch || isLoadingNextPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingNextPage = true;
|
||||
page++;
|
||||
try {
|
||||
var newVendors = await cakePayService.getVendors(
|
||||
countryCode: selectedCountry.countryCode,
|
||||
page: page,
|
||||
search: searchString,
|
||||
giftCards: displayGiftCards,
|
||||
prepaidCards: displayPrepaidCards);
|
||||
|
||||
cakePayVendors.addAll(newVendors.where((vendor) {
|
||||
if (vendor.card == null) return false;
|
||||
if (vendor.available != true) return false;
|
||||
return true;
|
||||
}).toList());
|
||||
} catch (error) {
|
||||
if (error.toString().contains('detail":"Invalid page."')) {
|
||||
hasMoreDataToFetch = false;
|
||||
}
|
||||
} finally {
|
||||
isLoadingNextPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isCakePayUserAuthenticated() async {
|
||||
return await cakePayService.isLogged();
|
||||
}
|
||||
|
||||
void resetLoadingNextPageState() {
|
||||
hasMoreDataToFetch = true;
|
||||
page = 1;
|
||||
}
|
||||
|
||||
void storeInitialFilterStates() {
|
||||
_initialSelectedCountry = selectedCountry;
|
||||
_initialDisplayPrepaidCards = displayPrepaidCards;
|
||||
_initialDisplayGiftCards = displayGiftCards;
|
||||
_initialDisplayDenominationsCards = displayDenominationsCards;
|
||||
_initialDisplayCustomValueCards = displayCustomValueCards;
|
||||
}
|
||||
|
||||
@action
|
||||
void setSelectedCountry(Country country) {
|
||||
// just so it triggers the reaction even when selecting the default country
|
||||
settingsStore.selectedCakePayCountry = null;
|
||||
settingsStore.selectedCakePayCountry = country;
|
||||
}
|
||||
|
||||
@action
|
||||
void setMyCardsQuery(String text) => searchMyCardsString = text;
|
||||
|
||||
@action
|
||||
void togglePrepaidCards() => displayPrepaidCards = !displayPrepaidCards;
|
||||
|
||||
@action
|
||||
void toggleGiftCards() => displayGiftCards = !displayGiftCards;
|
||||
|
||||
@action
|
||||
void toggleDenominationsCards() => displayDenominationsCards = !displayDenominationsCards;
|
||||
|
||||
@action
|
||||
void toggleCustomValueCards() => displayCustomValueCards = !displayCustomValueCards;
|
||||
|
||||
void setScrollOffsetFromTop(double scrollOffset) {
|
||||
scrollOffsetFromTop = scrollOffset;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
part 'cake_features_view_model.g.dart';
|
||||
|
||||
class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel;
|
||||
|
||||
abstract class CakeFeaturesViewModelBase with Store {
|
||||
final CakePayService _cakePayService;
|
||||
|
||||
CakeFeaturesViewModelBase(this._cakePayService);
|
||||
|
||||
Future<bool> isIoniaUserAuthenticated() async {
|
||||
return await _cakePayService.isLogged();
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,9 @@ import 'package:hash_wallet/core/amount_parsing_proxy.dart';
|
||||
import 'package:hash_wallet/entities/calculate_fiat_amount.dart';
|
||||
import 'package:hash_wallet/entities/fiat_currency.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
@@ -51,41 +41,12 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
isSwapsXYZCanSendFromExternal = _checkIfSwapsXYZCanSendFromExternal(tradesStore.trade!, wallet),
|
||||
items = ObservableList<ExchangeTradeItem>() {
|
||||
setUpOutput();
|
||||
// Trades from providers whose implementations were removed keep a null
|
||||
// _provider; status polling is simply skipped for them.
|
||||
switch (trade.provider) {
|
||||
case ExchangeProviderDescription.changeNow:
|
||||
_provider =
|
||||
ChangeNowExchangeProvider(settingsStore: sendViewModel.balanceViewModel.settingsStore);
|
||||
break;
|
||||
case ExchangeProviderDescription.sideShift:
|
||||
_provider = SideShiftExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.simpleSwap:
|
||||
_provider = SimpleSwapExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.trocador:
|
||||
_provider = TrocadorExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.exolix:
|
||||
_provider = ExolixExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.swapTrade:
|
||||
_provider = SwapTradeExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.stealthEx:
|
||||
_provider = StealthExExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.thorChain:
|
||||
_provider = ThorChainExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.chainflip:
|
||||
_provider = ChainflipExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.xoSwap:
|
||||
_provider = XOSwapExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.swapsXyz:
|
||||
_provider = SwapsXyzExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.nearIntents:
|
||||
_provider = NearIntentsExchangeProvider();
|
||||
break;
|
||||
@@ -195,7 +156,6 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
output = sendViewModel.outputs.first;
|
||||
output.address = trade.inputAddress ?? '';
|
||||
output.setCryptoAmount(trade.amount);
|
||||
if (_provider is ThorChainExchangeProvider) output.memo = trade.memo ?? "";
|
||||
if (trade.isSendAll == true) output.sendAll = true;
|
||||
}
|
||||
|
||||
@@ -211,19 +171,7 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
|
||||
sendViewModel.selectedCryptoCurrency = selected;
|
||||
|
||||
final pendingTransaction =
|
||||
await sendViewModel.createTransaction(provider: _provider, trade: trade);
|
||||
|
||||
if (_provider is SwapsXyzExchangeProvider) {
|
||||
final hash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
|
||||
trade.txId = hash;
|
||||
await trade.save();
|
||||
}
|
||||
|
||||
if (_provider is ThorChainExchangeProvider) {
|
||||
trade.id = pendingTransaction?.id ?? '';
|
||||
await trade.save();
|
||||
}
|
||||
await sendViewModel.createTransaction(provider: _provider, trade: trade);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -23,17 +23,8 @@ import 'package:hash_wallet/exchange/exchange_template.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_trade_state.dart';
|
||||
import 'package:hash_wallet/exchange/limits.dart';
|
||||
import 'package:hash_wallet/exchange/limits_state.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/exchange/trade_request.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
@@ -49,7 +40,6 @@ import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:hash_wallet/store/templates/exchange_template_store.dart';
|
||||
import 'package:hash_wallet/evm/evm.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/token_utilities.dart';
|
||||
import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
|
||||
@@ -197,8 +187,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
final initialProvider = provider;
|
||||
provider!.checkIsAvailable().then((bool isAvailable) {
|
||||
if (!isAvailable && provider == initialProvider) {
|
||||
provider = providerList.firstWhere((provider) => provider is ChangeNowExchangeProvider,
|
||||
orElse: () => providerList.last);
|
||||
provider = providerList.last;
|
||||
_onPairChange();
|
||||
}
|
||||
});
|
||||
@@ -323,20 +312,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
// Hash Bags: trimmed to Trocador only. Trocador's aggregator already fronts
|
||||
// ChangeNow, LetsExchange, Exolix, StealthEx, FixedFloat, Quantex, etc.,
|
||||
// letting us collect markup as the affiliate. Re-enable individual providers
|
||||
// here only after registering Such Software affiliate keys per provider.
|
||||
// letting us collect markup as the affiliate. The individual provider
|
||||
// implementations were deleted; restore them from Cake Wallet upstream and
|
||||
// register Such Software affiliate keys per provider before re-adding any.
|
||||
List<ExchangeProvider> get _allProviders => [
|
||||
// ChangeNowExchangeProvider(settingsStore: _settingsStore),
|
||||
// SideShiftExchangeProvider(),
|
||||
// ChainflipExchangeProvider(),
|
||||
// if (FeatureFlag.isExolixEnabled) ExolixExchangeProvider(),
|
||||
// SwapTradeExchangeProvider(),
|
||||
// LetsExchangeExchangeProvider(),
|
||||
// StealthExExchangeProvider(),
|
||||
// XOSwapExchangeProvider(),
|
||||
// SwapsXyzExchangeProvider(),
|
||||
// JupiterExchangeProvider(),
|
||||
// NearIntentsExchangeProvider(),
|
||||
TrocadorExchangeProvider(
|
||||
useTorOnly: _useTorOnly, providerStates: _settingsStore.trocadorProviderStates),
|
||||
];
|
||||
@@ -1501,16 +1480,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
|
||||
void _defineIsReceiveAmountEditable() {
|
||||
/*if ((provider is ChangeNowExchangeProvider)
|
||||
&&(depositCurrency == CryptoCurrency.xmr)
|
||||
&&(receiveCurrency == CryptoCurrency.btc)) {
|
||||
isReceiveAmountEditable = true;
|
||||
} else {
|
||||
isReceiveAmountEditable = false;
|
||||
}*/
|
||||
//isReceiveAmountEditable = false;
|
||||
// isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
|
||||
// isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
|
||||
isReceiveAmountEditable = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hash_wallet/buy/buy_provider_description.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/models/cake_pay_order.dart';
|
||||
import 'package:hash_wallet/cake_pay/src/services/cake_pay_service.dart';
|
||||
import 'package:hash_wallet/core/utilities.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/order/order.dart';
|
||||
import 'package:hash_wallet/order/order_provider.dart';
|
||||
import 'package:hash_wallet/order/order_provider_adapter/cake_pay_order_provider_adapter.dart';
|
||||
import 'package:hash_wallet/order/order_provider_description.dart';
|
||||
import 'package:hash_wallet/order/order_source_description.dart';
|
||||
import 'package:hash_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
|
||||
import 'package:hash_wallet/src/screens/trade_details/track_trade_list_item.dart';
|
||||
import 'package:hash_wallet/src/screens/trade_details/trade_details_status_item.dart';
|
||||
import 'package:hash_wallet/src/screens/transaction_details/standart_list_item.dart';
|
||||
import 'package:hash_wallet/utils/date_formatter.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_base.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -32,7 +23,6 @@ abstract class OrderDetailsViewModelBase with Store {
|
||||
OrderDetailsViewModelBase({
|
||||
required WalletBase wallet,
|
||||
required Order orderForDetails,
|
||||
required this.cakePayService,
|
||||
required this.orders,
|
||||
}) : items = ObservableList<StandartListItem>(),
|
||||
order = orderForDetails {
|
||||
@@ -44,9 +34,8 @@ abstract class OrderDetailsViewModelBase with Store {
|
||||
break;
|
||||
|
||||
case OrderSourceDescription.order:
|
||||
if (order.orderProvider == OrderProviderDescription.cakePay) {
|
||||
_provider = CakePayOrderProviderAdapter(wallet: wallet, cakePayService: cakePayService);
|
||||
}
|
||||
// Cake Pay orders: the Cake Pay integration was removed, so stored
|
||||
// orders are shown from persisted data only (_provider stays null).
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -61,10 +50,8 @@ abstract class OrderDetailsViewModelBase with Store {
|
||||
@observable
|
||||
ObservableList<StandartListItem> items;
|
||||
|
||||
final CakePayService cakePayService;
|
||||
final Box<Order> orders;
|
||||
OrderProvider? _provider;
|
||||
List<OrderCard> cards = [];
|
||||
|
||||
Timer? timer;
|
||||
|
||||
@@ -76,10 +63,6 @@ abstract class OrderDetailsViewModelBase with Store {
|
||||
final updatedOrderObj = await _provider!.findOrderById(order.id);
|
||||
final updatedOrder = updatedOrderObj.$1;
|
||||
|
||||
if (_provider is CakePayOrderProviderAdapter) {
|
||||
cards = updatedOrderObj.$2 as List<OrderCard>? ?? [];
|
||||
}
|
||||
|
||||
final existing = orders.values.firstWhereOrNull((e) => e.id == updatedOrder.id);
|
||||
if (existing != null) {
|
||||
existing.stateRaw = updatedOrder.stateRaw;
|
||||
@@ -98,30 +81,11 @@ abstract class OrderDetailsViewModelBase with Store {
|
||||
}
|
||||
|
||||
void _updateItems() {
|
||||
final dateFormat = DateFormatter.withCurrentLocal();
|
||||
items.clear();
|
||||
|
||||
items.add(
|
||||
DetailsListStatusItem(title: S.current.trade_details_state, value: order.state.toString()));
|
||||
|
||||
if (_provider is CakePayOrderProviderAdapter) {
|
||||
items.add(CakePayDetailsListCardItem(
|
||||
title: '',
|
||||
value: '',
|
||||
id: order.id,
|
||||
createdAt: dateFormat.format(order.createdAt),
|
||||
price: order.receiveAmount,
|
||||
quantity: order.quantity,
|
||||
from: order.from ?? '',
|
||||
to: order.to ?? '',
|
||||
cards: cards,
|
||||
onTap: (BuildContext context) {
|
||||
Clipboard.setData(ClipboardData(text: '${order.id}'));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
items.add(StandartListItem(title: 'Order provider', value: _provider?.title ?? ''));
|
||||
|
||||
final trackUrl = _provider?.trackUrl ?? '';
|
||||
|
||||
@@ -21,11 +21,8 @@ import 'package:hash_wallet/entities/template.dart';
|
||||
import 'package:hash_wallet/entities/transaction_description.dart';
|
||||
import 'package:hash_wallet/entities/wallet_contact.dart';
|
||||
import 'package:hash_wallet/evm/evm.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/monero/monero.dart';
|
||||
@@ -47,13 +44,11 @@ import 'package:hash_wallet/view_model/unspent_coins/unspent_coins_list_view_mod
|
||||
import 'package:hash_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/exceptions.dart';
|
||||
import 'package:cw_core/lnurl.dart';
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
@@ -131,10 +126,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
});
|
||||
}
|
||||
|
||||
// Store trade and provider references for post-commit updates (e.g., Jupiter trade ID update)
|
||||
Trade? _currentTrade;
|
||||
ExchangeProvider? _currentProvider;
|
||||
|
||||
@observable
|
||||
ExecutionState state;
|
||||
|
||||
@@ -596,8 +587,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
@action
|
||||
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
|
||||
_currentTrade = trade;
|
||||
_currentProvider = provider;
|
||||
pendingTransaction = null;
|
||||
|
||||
try {
|
||||
@@ -615,184 +604,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
}
|
||||
}
|
||||
|
||||
// Swaps.xyz (EVM) path
|
||||
if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) {
|
||||
final routerTo = trade.inputAddress;
|
||||
final routerData = trade.routerData;
|
||||
|
||||
|
||||
if (routerData != null && routerData != '0x') {
|
||||
final tokenContract = (trade.sourceTokenAddress ?? '').toLowerCase();
|
||||
final priority = _settingsStore.getPriority(
|
||||
walletType, chainId: selectedChainId);
|
||||
final routerValueWei = BigInt.tryParse(trade.routerValue ?? '0') ??
|
||||
BigInt.zero;
|
||||
|
||||
if (routerTo == null || routerTo.isEmpty) {
|
||||
state = FailureState('Invalid router address');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final selector = _decodeMethodSelector(routerData);
|
||||
const transferSig = '0xa9059cbb';
|
||||
const swapAndExecuteSig = '0x9be111d1';
|
||||
|
||||
// Direct Transfer (Simple routing, no approval needed)
|
||||
if (selector == transferSig) {
|
||||
pendingTransaction = await evm!.createRawCallDataTransaction(
|
||||
wallet,
|
||||
routerTo,
|
||||
routerData,
|
||||
BigInt.zero,
|
||||
priority,
|
||||
useBlinkProtection: canSupportBlinkProtection(selectedChainId)
|
||||
? _settingsStore.useBlinkProtection
|
||||
: false,
|
||||
);
|
||||
state = ExecutedSuccessfullyState();
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
// Smart Swap (Requires Approval)
|
||||
if (selector == swapAndExecuteSig) {
|
||||
final requiredAmount = BigInt.tryParse(
|
||||
(trade.sourceTokenAmountRaw ?? '0').replaceAll('n', '')) ??
|
||||
BigInt.zero;
|
||||
|
||||
final needsApproval = tokenContract.isNotEmpty &&
|
||||
requiredAmount > BigInt.zero
|
||||
? await evm!.isApprovalRequired(
|
||||
wallet, tokenContract, routerTo, requiredAmount)
|
||||
: false;
|
||||
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval required: $needsApproval for token ${trade
|
||||
.from?.title} ${trade.from?.tag ??
|
||||
''} with amount $requiredAmount');
|
||||
|
||||
if (needsApproval) {
|
||||
// USDT Approval Flow (Special Case). We must reset allowance to 0 first.
|
||||
final isUSDTMainnet = selectedChainId == 1 &&
|
||||
tokenContract.toLowerCase() ==
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7';
|
||||
|
||||
if (isUSDTMainnet) {
|
||||
final currentAllowance = await evm!.getAllowance(
|
||||
wallet, tokenContract, routerTo);
|
||||
|
||||
if (currentAllowance != null &&
|
||||
currentAllowance > BigInt.zero) {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] currentAllowance USDT: $currentAllowance. Resetting to 0 before setting new allowance.');
|
||||
|
||||
final resetTx = await buildApprovalNeeded(
|
||||
spender: routerTo,
|
||||
tokenContract: tokenContract,
|
||||
requiredAmount: BigInt.zero,
|
||||
// Approve 0
|
||||
sourceTokenDecimals: trade.sourceTokenDecimals,
|
||||
priority: priority
|
||||
);
|
||||
|
||||
if (resetTx != null) {
|
||||
await resetTx.commit();
|
||||
|
||||
final resetConfirmed = await _waitForApprovalUpdate(
|
||||
tokenContract: tokenContract,
|
||||
spender: routerTo,
|
||||
requiredAmount: BigInt.zero, // Wait until it equals 0
|
||||
waitForExactMatch: true,
|
||||
);
|
||||
|
||||
if (!resetConfirmed) {
|
||||
state = FailureState(
|
||||
'Failed to reset USDT allowance. Please try again.');
|
||||
return null;
|
||||
}
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] USDT allowance reset to 0 confirmed on-chain.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Standard Approval Flow
|
||||
final approvalTx = await buildApprovalNeeded(
|
||||
spender: routerTo,
|
||||
tokenContract: tokenContract,
|
||||
requiredAmount: requiredAmount,
|
||||
sourceTokenDecimals: trade.sourceTokenDecimals,
|
||||
priority: priority
|
||||
);
|
||||
|
||||
if (approvalTx == null) {
|
||||
state = FailureState('Failed to build approval transaction');
|
||||
return null;
|
||||
}
|
||||
|
||||
pendingTransaction = null;
|
||||
|
||||
try {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Submitting approval transaction for token ${trade
|
||||
.from?.title} ${trade.from?.tag ?? ''} ');
|
||||
await approvalTx.commit();
|
||||
|
||||
// Wait for the approval to be mined on-chain
|
||||
final isApproved = await _waitForApprovalUpdate(
|
||||
tokenContract: tokenContract,
|
||||
spender: routerTo,
|
||||
requiredAmount: requiredAmount,
|
||||
);
|
||||
|
||||
if (!isApproved) {
|
||||
state = FailureState(
|
||||
'Approval transaction failed or timed out on-chain. Try again.');
|
||||
return null;
|
||||
}
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval transaction confirmed on-chain. Proceeding with swap execution.');
|
||||
} catch (e, s) {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval transaction error: $e\n$s');
|
||||
state = FailureState(
|
||||
translateErrorMessage(e, wallet.type, wallet.currency));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct Final Swap Transaction
|
||||
printV('[Swaps.xyz sending flow] Building swap transaction');
|
||||
pendingTransaction = await evm!.createRawCallDataTransaction(
|
||||
wallet,
|
||||
routerTo,
|
||||
routerData,
|
||||
routerValueWei,
|
||||
priority,
|
||||
sourceTokenAddress: tokenContract,
|
||||
sourceTokenAmount: requiredAmount,
|
||||
useBlinkProtection: canSupportBlinkProtection(selectedChainId)
|
||||
? _settingsStore.useBlinkProtection
|
||||
: false,
|
||||
);
|
||||
|
||||
state = ExecutedSuccessfullyState();
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
state = FailureState('Unsupported Swaps.xyz transaction type');
|
||||
return null;
|
||||
} catch (e, s) {
|
||||
printV('Swaps.xyz transaction error: $e\n$s');
|
||||
state = FailureState(
|
||||
'Failed to create Swaps.xyz transaction - ${translateErrorMessage(
|
||||
e, wallet.type, wallet.currency)}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// END Swaps.xyz path
|
||||
|
||||
// Regular flow
|
||||
|
||||
|
||||
@@ -823,16 +634,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
}
|
||||
}
|
||||
|
||||
if (provider is ThorChainExchangeProvider) {
|
||||
final outputCount = pendingTransaction?.outputCount ?? 0;
|
||||
if (outputCount > 10) {
|
||||
throw Exception("THORChain does not support more than 10 outputs");
|
||||
}
|
||||
|
||||
if (_hasTaprootInput(pendingTransaction)) {
|
||||
throw Exception("THORChain does not support Taproot addresses");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -936,13 +737,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
state = TransactionCommitted();
|
||||
|
||||
if (_currentTrade != null) {
|
||||
final provider = _currentTrade!.provider;
|
||||
if (provider == ExchangeProviderDescription.swapsXyz) {
|
||||
registerSwapsXyzTransaction(_currentTrade!);
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate transaction update for EVM chains and Nano
|
||||
if (isEVMWallet || [WalletType.bitcoin, WalletType.nano].contains(walletType)) {
|
||||
Future.delayed(Duration(seconds: 4), () async {
|
||||
@@ -1121,88 +915,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper functions for EVM transaction monitoring and approval flow
|
||||
|
||||
// Polls the token contract directly to see if allowance is updated.
|
||||
Future<bool> _waitForApprovalUpdate({
|
||||
required String tokenContract,
|
||||
required String spender,
|
||||
required BigInt requiredAmount,
|
||||
bool waitForExactMatch = false,
|
||||
}) async {
|
||||
if (!isEVMWallet || evm == null) return false;
|
||||
|
||||
int attempts = 0;
|
||||
const int maxAttempts = 30; // ~60 seconds
|
||||
|
||||
printV('[Swaps.xyz sending flow] Starting allowance check. Target: $requiredAmount (Exact match: $waitForExactMatch)');
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
final currentAllowance = await evm!.getAllowance(wallet, tokenContract, spender);
|
||||
|
||||
if (currentAllowance != null) {
|
||||
printV('[Swaps.xyz sending flow] Current Allowance: $currentAllowance / Target: $requiredAmount');
|
||||
|
||||
if (waitForExactMatch) {
|
||||
// For Reset (Target 0): We need it to be exactly 0 (or less, though it can't be negative)
|
||||
if (currentAllowance <= requiredAmount) {
|
||||
printV('[Swaps.xyz sending flow] Allowance reset verified!');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// For Approval: We need it to be at least the required amount
|
||||
if (currentAllowance >= requiredAmount) {
|
||||
printV('[Swaps.xyz sending flow] Allowance verified!');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
printV('[Swaps.xyz sending flow] Allowance check error: $e');
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
printV('[Swaps.xyz sending flow] Allowance check timed out.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Builds a token approval transaction
|
||||
Future<PendingTransaction?> buildApprovalNeeded({
|
||||
required String spender,
|
||||
required String tokenContract,
|
||||
required BigInt requiredAmount,
|
||||
required TransactionPriority? priority,
|
||||
int? sourceTokenDecimals,
|
||||
}) async {
|
||||
|
||||
final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
|
||||
(t) => t.contractAddress.toLowerCase() == tokenContract.toLowerCase(),
|
||||
orElse: () => Erc20Token(
|
||||
name: '',
|
||||
symbol: '',
|
||||
contractAddress: tokenContract,
|
||||
decimal: sourceTokenDecimals ?? 18,
|
||||
enabled: true,
|
||||
),
|
||||
);
|
||||
|
||||
return await evm!.createTokenApproval(
|
||||
wallet,
|
||||
requiredAmount,
|
||||
spender,
|
||||
erc20Token,
|
||||
priority,
|
||||
useBlinkProtection:
|
||||
canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false,
|
||||
);
|
||||
}
|
||||
|
||||
// End EVM helper functions
|
||||
|
||||
String translateErrorMessage(
|
||||
Object error,
|
||||
WalletType walletType,
|
||||
@@ -1322,63 +1034,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _hasTaprootInput(PendingTransaction? pendingTransaction) {
|
||||
if (walletType == WalletType.bitcoin && pendingTransaction != null) {
|
||||
return bitcoin!.hasTaprootInput(pendingTransaction);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> registerSwapsXyzTransaction(Trade trade) async {
|
||||
try {
|
||||
|
||||
// register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz)
|
||||
final needToRegister = trade.needToRegisterInSwapXyz ?? false;
|
||||
if (!needToRegister) return;
|
||||
|
||||
final vmId = (trade.providerId ?? '').toLowerCase();
|
||||
if (vmId.isEmpty) {
|
||||
printV('SwapsXyz: transaction register: skipped (vmId empty)');
|
||||
return;
|
||||
}
|
||||
|
||||
final txHash = pendingTransaction?.evmTxHashFromRawHex
|
||||
?? pendingTransaction?.id
|
||||
?? '';
|
||||
|
||||
if (txHash.isEmpty) {
|
||||
printV('SwapsXyz: transaction register: skipped (txHash empty)');
|
||||
return;
|
||||
}
|
||||
|
||||
final chainId = int.tryParse(trade.router ?? '') ?? 0;
|
||||
if (chainId <= 0) {
|
||||
printV('SwapsXyz: transaction register: skipped (invalid chainId)');
|
||||
return;
|
||||
}
|
||||
|
||||
printV(
|
||||
'SwapsXyz: attempting to register transaction: tradeId = ${trade.id}, txHash = $txHash, chainId = $chainId, vmId = $vmId');
|
||||
|
||||
final registered = await SwapsXyzExchangeProvider.registerAltVmTx(
|
||||
txId: trade.id,
|
||||
txHash: txHash,
|
||||
chainId: chainId,
|
||||
vmId: vmId,
|
||||
);
|
||||
|
||||
if (!registered) {
|
||||
printV('SwapsXyz: transaction register: failed');
|
||||
} else {
|
||||
printV('SwapsXyz: transaction register: success');
|
||||
}
|
||||
} catch (e) {
|
||||
printV('registerSwapsXyzTransaction error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get usePayjoin => _settingsStore.usePayjoin;
|
||||
|
||||
@@ -1397,6 +1052,4 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
}
|
||||
}
|
||||
|
||||
String _decodeMethodSelector(String s) =>
|
||||
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/changenow_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/exolix_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/letsexchange_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/near_Intents_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/swaptrade_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/sideshift_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/simpleswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/stealth_ex_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/trocador_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/trade_details/track_trade_list_item.dart';
|
||||
@@ -40,42 +29,12 @@ abstract class TradeDetailsViewModelBase with Store {
|
||||
required this.appStore,
|
||||
}) : items = ObservableList<StandartListItem>(),
|
||||
trade = tradeForDetails {
|
||||
// Trades from providers whose implementations were removed keep a null
|
||||
// _provider and show the "provider unsupported" item with stored data.
|
||||
switch (trade.provider) {
|
||||
case ExchangeProviderDescription.changeNow:
|
||||
_provider = ChangeNowExchangeProvider(settingsStore: appStore.settingsStore);
|
||||
break;
|
||||
case ExchangeProviderDescription.sideShift:
|
||||
_provider = SideShiftExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.simpleSwap:
|
||||
_provider = SimpleSwapExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.trocador:
|
||||
_provider = TrocadorExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.exolix:
|
||||
_provider = ExolixExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.thorChain:
|
||||
_provider = ThorChainExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.swapTrade:
|
||||
_provider = SwapTradeExchangeProvider();
|
||||
case ExchangeProviderDescription.letsExchange:
|
||||
_provider = LetsExchangeExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.stealthEx:
|
||||
_provider = StealthExExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.chainflip:
|
||||
_provider = ChainflipExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.xoSwap:
|
||||
_provider = XOSwapExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.swapsXyz:
|
||||
_provider = SwapsXyzExchangeProvider();
|
||||
break;
|
||||
case ExchangeProviderDescription.nearIntents:
|
||||
_provider = NearIntentsExchangeProvider();
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user