/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:async'; import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../app_config.dart'; import '../../../models/buy/response_objects/crypto.dart'; import '../../../models/buy/response_objects/fiat.dart'; import '../../../models/buy/response_objects/order.dart'; import '../../../models/buy/response_objects/quote.dart'; import '../../../networking/http.dart'; import '../../../utilities/enums/fiat_enum.dart'; import '../../../utilities/logger.dart'; import '../../../utilities/prefs.dart'; import '../../tor_service.dart'; import '../buy_response.dart'; class SimplexAPI { static const String authority = "buycrypto.stackwallet.com"; // static const String authority = "localhost"; // For development purposes static const String scheme = authority == "localhost" ? "http" : "https"; final _prefs = Prefs.instance; SimplexAPI._(); static final SimplexAPI _instance = SimplexAPI._(); static SimplexAPI get instance => _instance; HTTP client = HTTP(); Uri _buildUri(String path, Map? params) { if (scheme == "http") { return Uri.http(authority, path, params); } return Uri.https(authority, path, params); } Future>> getSupportedCryptos() async { try { final Map headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; final Map data = { 'ROUTE': 'supported_cryptos', }; final Uri url = _buildUri('api.php', data); final res = await client.post( url: url, headers: headers, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); if (res.code != 200) { throw Exception( 'getAvailableCurrencies exception: statusCode= ${res.code}', ); } final jsonArray = jsonDecode(res.body); // TODO handle if invalid json return _parseSupportedCryptos(jsonArray); } catch (e, s) { Logging.instance.log( "getAvailableCurrencies exception: $e\n$s", level: LogLevel.Error, ); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } BuyResponse> _parseSupportedCryptos(dynamic jsonArray) { try { final List cryptos = []; for (final crypto in jsonArray as List) { // TODO validate jsonArray if (AppConfig.isStackCoin("${crypto['ticker_symbol']}")) { cryptos.add( Crypto.fromJson({ 'ticker': "${crypto['ticker_symbol']}", 'name': crypto['name'], 'network': "${crypto['network']}", 'contractAddress': "${crypto['contractAddress']}", 'image': "", }), ); } } return BuyResponse(value: cryptos); } catch (e, s) { Logging.instance .log("_parseSupported exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } Future>> getSupportedFiats() async { try { final Map headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; final Map data = { 'ROUTE': 'supported_fiats', }; final Uri url = _buildUri('api.php', data); final res = await client.post( url: url, headers: headers, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); if (res.code != 200) { throw Exception( 'getAvailableCurrencies exception: statusCode= ${res.code}', ); } final jsonArray = jsonDecode(res.body); // TODO validate json return _parseSupportedFiats(jsonArray); } catch (e, s) { Logging.instance.log( "getAvailableCurrencies exception: $e\n$s", level: LogLevel.Error, ); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } BuyResponse> _parseSupportedFiats(dynamic jsonArray) { try { final List cryptos = []; final List fiats = []; for (final fiat in jsonArray as List) { if (isSimplexFiat("${fiat['ticker_symbol']}")) { // TODO validate list fiats.add( Fiat.fromJson({ 'ticker': "${fiat['ticker_symbol']}", 'name': fiatFromTickerCaseInsensitive("${fiat['ticker_symbol']}") .prettyName, 'minAmount': "${fiat['min_amount']}", 'maxAmount': "${fiat['max_amount']}", 'image': "", }), ); } // TODO handle else } return BuyResponse(value: fiats); } catch (e, s) { Logging.instance .log("_parseSupported exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } Future> getQuote(SimplexQuote quote) async { try { await _prefs.init(); final String? userID = _prefs.userID; final Map headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; final Map data = { 'ROUTE': 'quote', 'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(), 'FIAT_TICKER': quote.fiat.ticker.toUpperCase(), 'REQUESTED_TICKER': quote.buyWithFiat ? quote.fiat.ticker.toUpperCase() : quote.crypto.ticker.toUpperCase(), 'REQUESTED_AMOUNT': quote.buyWithFiat ? "${quote.youPayFiatPrice}" : "${quote.youReceiveCryptoAmount}", }; if (userID != null) { data['USER_ID'] = userID; } final Uri url = _buildUri('api.php', data); final res = await client.get( url: url, headers: headers, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); if (res.code != 200) { throw Exception('getQuote exception: statusCode= ${res.code}'); } final jsonArray = jsonDecode(res.body); if (jsonArray.containsKey('error') as bool) { if (jsonArray['error'] == true || jsonArray['error'] == 'true') { // jsonArray['error'] as bool == true? throw Exception('getQuote exception: ${jsonArray['error']}'); } } jsonArray['quote'] = quote; // Add and pass this on return _parseQuote(jsonArray); } catch (e, s) { Logging.instance.log("getQuote exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } BuyResponse _parseQuote(dynamic jsonArray) { try { // final Map lol = // Map.from(jsonArray as Map); final double? cryptoAmount = jsonArray['digital_money']?['amount'] as double?; if (cryptoAmount == null) { final String error = jsonArray['error'] as String; return BuyResponse( exception: BuyException( error, BuyExceptionType.cryptoAmountOutOfRange, ), ); } final SimplexQuote quote = jsonArray['quote'] as SimplexQuote; final SimplexQuote _quote = SimplexQuote( crypto: quote.crypto, fiat: quote.fiat, youPayFiatPrice: quote.buyWithFiat ? quote.youPayFiatPrice : Decimal.parse("${jsonArray['fiat_money']['base_amount']}"), youReceiveCryptoAmount: Decimal.parse("${jsonArray['digital_money']['amount']}"), id: jsonArray['quote_id'] as String, receivingAddress: quote.receivingAddress, buyWithFiat: quote.buyWithFiat, ); return BuyResponse(value: _quote); } catch (e, s) { Logging.instance .log("_parseQuote exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } Future> newOrder(SimplexQuote quote) async { // Calling Simplex's API manually: // curl --request POST \ // --url https://sandbox.test-simplexcc.com/wallet/merchant/v2/payments/partner/data \ // --header 'Authorization: ApiKey $apiKey' \ // --header 'accept: application/json' \ // --header 'content-type: application/json' \ // -d '{"account_details": {"app_provider_id": "$publicKey", "app_version_id": "123", "app_end_user_id": "01e7a0b9-8dfc-4988-a28d-84a34e5f0a63", "signup_login": {"timestamp": "1994-11-05T08:15:30-05:00", "ip": "207.66.86.226"}}, "transaction_details": {"payment_details": {"quote_id": "3b58f4b4-ed6f-447c-b96a-ffe97d7b6803", "payment_id": "baaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "order_id": "789", "original_http_ref_url": "https://stackwallet.com/simplex", "destination_wallet": {"currency": "BTC", "address": "bc1qjvj9ca8gdsv3g58yrzrk6jycvgnjh9uj35rja2"}}}}' try { await _prefs.init(); final String? userID = _prefs.userID; final int? signupEpoch = _prefs.signupEpoch; final Map headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; final Map data = { 'ROUTE': 'order', 'QUOTE_ID': quote.id, 'ADDRESS': quote.receivingAddress, 'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(), }; if (userID != null) { data['USER_ID'] = userID; } if (signupEpoch != null && signupEpoch != 0) { final DateTime date = DateTime.fromMillisecondsSinceEpoch(signupEpoch * 1000); data['SIGNUP_TIMESTAMP'] = date.toIso8601String() + timeZoneFormatter(date.timeZoneOffset); } final Uri url = _buildUri('api.php', data); final res = await client.get( url: url, headers: headers, proxyInfo: Prefs.instance.useTor ? TorService.sharedInstance.getProxyInfo() : null, ); if (res.code != 200) { throw Exception('newOrder exception: statusCode= ${res.code}'); } final jsonArray = jsonDecode(res.body); // TODO check if valid json if (jsonArray.containsKey('error') as bool) { if (jsonArray['error'] == true || jsonArray['error'] == 'true') { throw Exception(jsonArray['message']); } } final SimplexOrder _order = SimplexOrder( quote: quote, paymentId: "${jsonArray['paymentId']}", orderId: "${jsonArray['orderId']}", userId: "${jsonArray['userId']}", ); return BuyResponse(value: _order); } catch (e, s) { Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } Future> redirect(SimplexOrder order) async { try { final Map headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; final Map data = { 'ROUTE': 'redirect', 'PAYMENT_ID': order.paymentId, }; final Uri url = _buildUri('api.php', data); final bool status = await launchUrl( url, mode: LaunchMode.externalApplication, ); return BuyResponse(value: status); } catch (e, s) { Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error); return BuyResponse( exception: BuyException( e.toString(), BuyExceptionType.generic, ), ); } } bool isSimplexFiat(String ticker) { try { fiatFromTickerCaseInsensitive(ticker); return true; } on ArgumentError catch (_) { return false; } } // See https://github.com/dart-lang/sdk/issues/43391#issuecomment-1229656422 String timeZoneFormatter(Duration offset) => "${offset.isNegative ? "-" : "+"}${offset.inHours.abs().toString().padLeft(2, "0")}:${(offset.inMinutes - offset.inHours * 60).abs().toString().padLeft(2, "0")}"; }