2023-05-26 21:21:16 +00:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2023-01-20 05:10:15 +00:00
|
|
|
import 'dart:async';
|
2023-01-11 23:04:03 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2023-01-16 23:38:42 +00:00
|
|
|
import 'package:decimal/decimal.dart';
|
2023-01-11 23:04:03 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
2023-01-14 00:07:27 +00:00
|
|
|
import 'package:stackwallet/models/buy/response_objects/crypto.dart';
|
2023-01-11 23:04:03 +00:00
|
|
|
import 'package:stackwallet/models/buy/response_objects/fiat.dart';
|
2023-01-20 05:10:15 +00:00
|
|
|
import 'package:stackwallet/models/buy/response_objects/order.dart';
|
2023-01-16 23:38:42 +00:00
|
|
|
import 'package:stackwallet/models/buy/response_objects/quote.dart';
|
2023-01-11 23:04:03 +00:00
|
|
|
import 'package:stackwallet/services/buy/buy_response.dart';
|
2023-01-26 23:08:57 +00:00
|
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
2023-01-24 16:05:15 +00:00
|
|
|
import 'package:stackwallet/utilities/enums/fiat_enum.dart';
|
2023-01-11 23:04:03 +00:00
|
|
|
import 'package:stackwallet/utilities/logger.dart';
|
2023-01-24 22:02:25 +00:00
|
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
2023-01-20 15:27:24 +00:00
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2023-01-11 23:04:03 +00:00
|
|
|
|
|
|
|
class SimplexAPI {
|
2023-02-07 16:32:46 +00:00
|
|
|
static const String authority = "buycrypto.stackwallet.com";
|
2023-01-27 21:23:14 +00:00
|
|
|
// static const String authority = "localhost"; // For development purposes
|
2023-01-25 16:52:41 +00:00
|
|
|
static const String scheme = authority == "localhost" ? "http" : "https";
|
2023-01-11 23:04:03 +00:00
|
|
|
|
2023-01-24 22:02:25 +00:00
|
|
|
final _prefs = Prefs.instance;
|
|
|
|
|
2023-01-11 23:04:03 +00:00
|
|
|
SimplexAPI._();
|
|
|
|
static final SimplexAPI _instance = SimplexAPI._();
|
|
|
|
static SimplexAPI get instance => _instance;
|
|
|
|
|
|
|
|
/// set this to override using standard http client. Useful for testing
|
|
|
|
http.Client? client;
|
|
|
|
|
|
|
|
Uri _buildUri(String path, Map<String, String>? params) {
|
2023-01-25 16:52:41 +00:00
|
|
|
if (scheme == "http") {
|
|
|
|
return Uri.http(authority, path, params);
|
|
|
|
}
|
2023-01-11 23:04:03 +00:00
|
|
|
return Uri.https(authority, path, params);
|
|
|
|
}
|
|
|
|
|
2023-01-20 21:30:35 +00:00
|
|
|
Future<BuyResponse<List<Crypto>>> getSupportedCryptos() async {
|
2023-01-11 23:04:03 +00:00
|
|
|
try {
|
2023-01-12 20:38:03 +00:00
|
|
|
Map<String, String> headers = {
|
2023-01-20 21:30:35 +00:00
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
2023-01-12 20:14:53 +00:00
|
|
|
};
|
2023-01-25 16:52:41 +00:00
|
|
|
Map<String, String> data = {
|
|
|
|
'ROUTE': 'supported_cryptos',
|
|
|
|
};
|
|
|
|
Uri url = _buildUri('api.php', data);
|
2023-01-12 20:38:03 +00:00
|
|
|
|
2023-01-20 21:30:35 +00:00
|
|
|
var res = await http.post(url, headers: headers);
|
2023-01-12 20:38:03 +00:00
|
|
|
if (res.statusCode != 200) {
|
|
|
|
throw Exception(
|
|
|
|
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
|
|
|
|
}
|
2023-01-21 06:19:56 +00:00
|
|
|
final jsonArray = jsonDecode(res.body); // TODO handle if invalid json
|
2023-01-12 00:13:34 +00:00
|
|
|
|
2023-01-25 16:52:41 +00:00
|
|
|
return _parseSupportedCryptos(jsonArray);
|
2023-01-11 23:04:03 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
|
|
|
|
level: LogLevel.Error);
|
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-20 21:30:35 +00:00
|
|
|
BuyResponse<List<Crypto>> _parseSupportedCryptos(dynamic jsonArray) {
|
2023-01-11 23:04:03 +00:00
|
|
|
try {
|
2023-01-14 00:07:27 +00:00
|
|
|
List<Crypto> cryptos = [];
|
2023-01-12 21:15:42 +00:00
|
|
|
List<Fiat> fiats = [];
|
2023-01-11 23:04:03 +00:00
|
|
|
|
2023-01-20 21:30:35 +00:00
|
|
|
for (final crypto in jsonArray as List) {
|
2023-01-21 06:19:56 +00:00
|
|
|
// TODO validate jsonArray
|
2023-01-26 23:08:57 +00:00
|
|
|
if (isStackCoin("${crypto['ticker_symbol']}")) {
|
|
|
|
cryptos.add(Crypto.fromJson({
|
|
|
|
'ticker': "${crypto['ticker_symbol']}",
|
|
|
|
'name': crypto['name'],
|
|
|
|
'network': "${crypto['network']}",
|
|
|
|
'contractAddress': "${crypto['contractAddress']}",
|
|
|
|
'image': "",
|
|
|
|
}));
|
|
|
|
}
|
2023-01-14 00:07:27 +00:00
|
|
|
}
|
2023-01-20 21:30:35 +00:00
|
|
|
|
|
|
|
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<BuyResponse<List<Fiat>>> getSupportedFiats() async {
|
|
|
|
try {
|
|
|
|
Map<String, String> headers = {
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
};
|
2023-01-25 17:05:51 +00:00
|
|
|
Map<String, String> data = {
|
|
|
|
'ROUTE': 'supported_fiats',
|
|
|
|
};
|
|
|
|
Uri url = _buildUri('api.php', data);
|
2023-01-20 21:30:35 +00:00
|
|
|
|
|
|
|
var res = await http.post(url, headers: headers);
|
|
|
|
if (res.statusCode != 200) {
|
|
|
|
throw Exception(
|
|
|
|
'getAvailableCurrencies exception: statusCode= ${res.statusCode}');
|
|
|
|
}
|
2023-01-21 06:19:56 +00:00
|
|
|
final jsonArray = jsonDecode(res.body); // TODO validate json
|
2023-01-20 21:30:35 +00:00
|
|
|
|
2023-01-25 17:05:51 +00:00
|
|
|
return _parseSupportedFiats(jsonArray);
|
2023-01-20 21:30:35 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("getAvailableCurrencies exception: $e\n$s",
|
|
|
|
level: LogLevel.Error);
|
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BuyResponse<List<Fiat>> _parseSupportedFiats(dynamic jsonArray) {
|
|
|
|
try {
|
|
|
|
List<Crypto> cryptos = [];
|
|
|
|
List<Fiat> fiats = [];
|
|
|
|
|
2023-01-20 21:45:37 +00:00
|
|
|
for (final fiat in jsonArray as List) {
|
2023-01-24 16:05:15 +00:00
|
|
|
if (isSimplexFiat("${fiat['ticker_symbol']}")) {
|
|
|
|
// TODO validate list
|
|
|
|
fiats.add(Fiat.fromJson({
|
|
|
|
'ticker': "${fiat['ticker_symbol']}",
|
|
|
|
'name': fiatFromTickerCaseInsensitive("${fiat['ticker_symbol']}")
|
|
|
|
.prettyName,
|
2023-01-25 19:48:38 +00:00
|
|
|
'minAmount': "${fiat['min_amount']}",
|
|
|
|
'maxAmount': "${fiat['max_amount']}",
|
2023-01-24 16:05:15 +00:00
|
|
|
'image': "",
|
|
|
|
}));
|
|
|
|
} // TODO handle else
|
2023-01-14 00:07:27 +00:00
|
|
|
}
|
2023-01-13 02:21:19 +00:00
|
|
|
|
2023-01-20 21:30:35 +00:00
|
|
|
return BuyResponse(value: fiats);
|
2023-01-11 23:04:03 +00:00
|
|
|
} catch (e, s) {
|
2023-01-13 02:21:19 +00:00
|
|
|
Logging.instance
|
|
|
|
.log("_parseSupported exception: $e\n$s", level: LogLevel.Error);
|
2023-01-11 23:04:03 +00:00
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-16 23:38:42 +00:00
|
|
|
Future<BuyResponse<SimplexQuote>> getQuote(SimplexQuote quote) async {
|
|
|
|
try {
|
2023-01-24 22:02:25 +00:00
|
|
|
await _prefs.init();
|
|
|
|
String? userID = _prefs.userID;
|
|
|
|
|
2023-01-16 23:38:42 +00:00
|
|
|
Map<String, String> headers = {
|
2023-01-20 01:21:06 +00:00
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
};
|
|
|
|
Map<String, String> 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}",
|
2023-01-16 23:38:42 +00:00
|
|
|
};
|
2023-01-24 22:02:25 +00:00
|
|
|
if (userID != null) {
|
|
|
|
data['USER_ID'] = userID;
|
|
|
|
}
|
2023-01-25 17:06:27 +00:00
|
|
|
Uri url = _buildUri('api.php', data);
|
2023-01-16 23:38:42 +00:00
|
|
|
|
2023-01-23 16:59:14 +00:00
|
|
|
var res = await http.get(url, headers: headers);
|
2023-01-16 23:38:42 +00:00
|
|
|
if (res.statusCode != 200) {
|
2023-01-17 00:49:15 +00:00
|
|
|
throw Exception('getQuote exception: statusCode= ${res.statusCode}');
|
2023-01-16 23:38:42 +00:00
|
|
|
}
|
|
|
|
final jsonArray = jsonDecode(res.body);
|
2023-01-26 20:20:45 +00:00
|
|
|
if (jsonArray.containsKey('error') as bool) {
|
2023-01-27 22:08:56 +00:00
|
|
|
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
|
|
|
|
// jsonArray['error'] as bool == true?
|
|
|
|
throw Exception('getQuote exception: ${jsonArray['error']}');
|
|
|
|
}
|
2023-01-26 20:20:45 +00:00
|
|
|
}
|
2023-01-16 23:38:42 +00:00
|
|
|
|
2023-01-17 00:08:37 +00:00
|
|
|
jsonArray['quote'] = quote; // Add and pass this on
|
|
|
|
|
2023-01-25 18:13:20 +00:00
|
|
|
return _parseQuote(jsonArray);
|
2023-01-16 23:38:42 +00:00
|
|
|
} catch (e, s) {
|
2023-01-17 00:49:15 +00:00
|
|
|
Logging.instance.log("getQuote exception: $e\n$s", level: LogLevel.Error);
|
2023-01-16 23:38:42 +00:00
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BuyResponse<SimplexQuote> _parseQuote(dynamic jsonArray) {
|
|
|
|
try {
|
2023-01-30 20:08:21 +00:00
|
|
|
// final Map<String, dynamic> lol =
|
|
|
|
// Map<String, dynamic>.from(jsonArray as Map);
|
|
|
|
|
2023-01-31 17:09:21 +00:00
|
|
|
double? cryptoAmount = jsonArray['digital_money']?['amount'] as double?;
|
2023-01-30 20:08:21 +00:00
|
|
|
|
|
|
|
if (cryptoAmount == null) {
|
|
|
|
String error = jsonArray['error'] as String;
|
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
error,
|
|
|
|
BuyExceptionType.cryptoAmountOutOfRange,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-01-16 23:38:42 +00:00
|
|
|
|
2023-01-17 00:08:37 +00:00
|
|
|
SimplexQuote quote = jsonArray['quote'] as SimplexQuote;
|
|
|
|
final SimplexQuote _quote = SimplexQuote(
|
2023-01-19 19:36:05 +00:00
|
|
|
crypto: quote.crypto,
|
|
|
|
fiat: quote.fiat,
|
2023-01-24 17:55:36 +00:00
|
|
|
youPayFiatPrice: quote.buyWithFiat
|
|
|
|
? quote.youPayFiatPrice
|
|
|
|
: Decimal.parse("${jsonArray['fiat_money']['base_amount']}"),
|
|
|
|
youReceiveCryptoAmount:
|
|
|
|
Decimal.parse("${jsonArray['digital_money']['amount']}"),
|
2023-01-19 23:47:27 +00:00
|
|
|
id: jsonArray['quote_id'] as String,
|
2023-01-19 19:36:05 +00:00
|
|
|
receivingAddress: quote.receivingAddress,
|
|
|
|
buyWithFiat: quote.buyWithFiat,
|
|
|
|
);
|
2023-01-16 23:38:42 +00:00
|
|
|
|
2023-01-17 00:08:37 +00:00
|
|
|
return BuyResponse(value: _quote);
|
2023-01-16 23:38:42 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance
|
2023-01-17 00:49:15 +00:00
|
|
|
.log("_parseQuote exception: $e\n$s", level: LogLevel.Error);
|
2023-01-16 23:38:42 +00:00
|
|
|
return BuyResponse(
|
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 00:49:03 +00:00
|
|
|
|
2023-01-20 05:10:15 +00:00
|
|
|
Future<BuyResponse<SimplexOrder>> newOrder(SimplexQuote quote) async {
|
2023-01-20 01:21:06 +00:00
|
|
|
// 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"}}}}'
|
2023-01-17 00:49:03 +00:00
|
|
|
try {
|
2023-01-25 17:08:22 +00:00
|
|
|
await _prefs.init();
|
|
|
|
String? userID = _prefs.userID;
|
|
|
|
int? signupEpoch = _prefs.signupEpoch;
|
|
|
|
|
2023-01-17 00:49:03 +00:00
|
|
|
Map<String, String> headers = {
|
2023-01-20 01:21:06 +00:00
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
2023-01-17 00:49:03 +00:00
|
|
|
};
|
2023-01-20 01:21:06 +00:00
|
|
|
Map<String, String> data = {
|
|
|
|
'ROUTE': 'order',
|
2023-01-25 18:00:02 +00:00
|
|
|
'QUOTE_ID': quote.id,
|
|
|
|
'ADDRESS': quote.receivingAddress,
|
|
|
|
'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(),
|
2023-01-20 01:21:06 +00:00
|
|
|
};
|
2023-01-25 17:08:22 +00:00
|
|
|
if (userID != null) {
|
|
|
|
data['USER_ID'] = userID;
|
|
|
|
}
|
|
|
|
if (signupEpoch != null && signupEpoch != 0) {
|
2023-01-25 18:00:02 +00:00
|
|
|
DateTime date = DateTime.fromMillisecondsSinceEpoch(signupEpoch * 1000);
|
2023-01-25 17:08:22 +00:00
|
|
|
data['SIGNUP_TIMESTAMP'] =
|
2023-01-25 18:00:02 +00:00
|
|
|
date.toIso8601String() + timeZoneFormatter(date.timeZoneOffset);
|
2023-01-25 17:08:22 +00:00
|
|
|
}
|
|
|
|
Uri url = _buildUri('api.php', data);
|
2023-01-17 00:49:03 +00:00
|
|
|
|
2023-01-23 16:59:14 +00:00
|
|
|
var res = await http.get(url, headers: headers);
|
2023-01-17 00:49:03 +00:00
|
|
|
if (res.statusCode != 200) {
|
|
|
|
throw Exception('newOrder exception: statusCode= ${res.statusCode}');
|
|
|
|
}
|
2023-01-20 15:27:24 +00:00
|
|
|
final jsonArray = jsonDecode(res.body); // TODO check if valid json
|
2023-01-27 22:08:56 +00:00
|
|
|
if (jsonArray.containsKey('error') as bool) {
|
2023-01-30 20:08:21 +00:00
|
|
|
if (jsonArray['error'] == true || jsonArray['error'] == 'true') {
|
|
|
|
throw Exception(jsonArray['message']);
|
|
|
|
}
|
2023-01-27 22:08:56 +00:00
|
|
|
}
|
2023-01-21 04:49:14 +00:00
|
|
|
|
2023-01-20 05:10:15 +00:00
|
|
|
SimplexOrder _order = SimplexOrder(
|
|
|
|
quote: quote,
|
|
|
|
paymentId: "${jsonArray['paymentId']}",
|
|
|
|
orderId: "${jsonArray['orderId']}",
|
|
|
|
userId: "${jsonArray['userId']}",
|
|
|
|
);
|
|
|
|
|
|
|
|
return BuyResponse(value: _order);
|
2023-01-17 00:49:03 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error);
|
2023-01-20 05:10:15 +00:00
|
|
|
return BuyResponse(
|
2023-01-17 00:49:03 +00:00
|
|
|
exception: BuyException(
|
|
|
|
e.toString(),
|
|
|
|
BuyExceptionType.generic,
|
|
|
|
),
|
2023-01-20 05:10:15 +00:00
|
|
|
);
|
2023-01-17 00:49:03 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-20 15:27:24 +00:00
|
|
|
|
|
|
|
Future<BuyResponse<bool>> redirect(SimplexOrder order) async {
|
|
|
|
try {
|
2023-01-25 17:13:00 +00:00
|
|
|
Map<String, String> headers = {
|
|
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
|
|
};
|
|
|
|
Map<String, String> data = {
|
|
|
|
'ROUTE': 'redirect',
|
|
|
|
'PAYMENT_ID': order.paymentId,
|
|
|
|
};
|
|
|
|
Uri url = _buildUri('api.php', data);
|
|
|
|
|
2023-01-20 15:27:24 +00:00
|
|
|
bool status = await launchUrl(
|
2023-01-25 17:13:00 +00:00
|
|
|
url,
|
2023-01-20 15:27:24 +00:00
|
|
|
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,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
2023-01-24 16:05:15 +00:00
|
|
|
|
|
|
|
bool isSimplexFiat(String ticker) {
|
|
|
|
try {
|
|
|
|
fiatFromTickerCaseInsensitive(ticker);
|
|
|
|
return true;
|
|
|
|
} on ArgumentError catch (_) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2023-01-25 18:00:02 +00:00
|
|
|
|
|
|
|
// 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")}";
|
2023-01-11 23:04:03 +00:00
|
|
|
}
|
2023-01-26 23:08:57 +00:00
|
|
|
|
|
|
|
bool isStackCoin(String? ticker) {
|
|
|
|
if (ticker == null) return false;
|
|
|
|
|
|
|
|
try {
|
|
|
|
coinFromTickerCaseInsensitive(ticker);
|
|
|
|
return true;
|
|
|
|
} on ArgumentError catch (_) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|