Skip to content

Commit

Permalink
Merge pull request #139 from KomodoPlatform/fix/binance-geo-restrictions
Browse files Browse the repository at this point in the history
fix(market-metrics): add support for fallback URLs for Binance geo-blocking
  • Loading branch information
ca333 authored Sep 8, 2024
2 parents 766a38b + 67e4e07 commit 78f3f14
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class BinanceProvider {
int? startUnixTimestampMilliseconds,
int? endUnixTimestampMilliseconds,
int? limit,
String? baseUrl,
}) async {
final queryParameters = <String, dynamic>{
'symbol': symbol,
Expand All @@ -65,8 +66,9 @@ class BinanceProvider {
if (limit != null) 'limit': limit.toString(),
};

final uri =
Uri.parse('$apiUrl/klines').replace(queryParameters: queryParameters);
final baseRequestUrl = baseUrl ?? apiUrl;
final uri = Uri.parse('$baseRequestUrl/klines')
.replace(queryParameters: queryParameters);

final response = await http.get(uri);
if (response.statusCode == 200) {
Expand All @@ -84,10 +86,11 @@ class BinanceProvider {
///
/// Returns a [Future] that resolves to a [BinanceExchangeInfoResponse] object
/// Throws an [Exception] if the request fails.
Future<BinanceExchangeInfoResponse> fetchExchangeInfo() async {
final response = await http.get(
Uri.parse('$apiUrl/exchangeInfo'),
);
Future<BinanceExchangeInfoResponse> fetchExchangeInfo({
String? baseUrl,
}) async {
final requestUrl = baseUrl ?? apiUrl;
final response = await http.get(Uri.parse('$requestUrl/exchangeInfo'));

if (response.statusCode == 200) {
return BinanceExchangeInfoResponse.fromJson(
Expand All @@ -105,15 +108,23 @@ class BinanceProvider {
/// Returns a [Future] that resolves to a [BinanceExchangeInfoResponseReduced]
/// object.
/// Throws an [Exception] if the request fails.
Future<BinanceExchangeInfoResponseReduced> fetchExchangeInfoReduced() async {
final response = await http.get(
Uri.parse('$apiUrl/exchangeInfo'),
);
Future<BinanceExchangeInfoResponseReduced> fetchExchangeInfoReduced({
String? baseUrl,
}) async {
final requestUrl = baseUrl ?? apiUrl;
final response = await http.get(Uri.parse('$requestUrl/exchangeInfo'));

if (response.statusCode == 200) {
return BinanceExchangeInfoResponseReduced.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
} else if (response.statusCode == 451) {
// service unavailable for legal reasons
return BinanceExchangeInfoResponseReduced(
timezone: '',
serverTime: 0,
symbols: List.empty(),
);
} else {
throw http.ClientException(
'Failed to load exchange info: ${response.statusCode} ${response.body}',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Using relative imports in this "package" to make it easier to track external
// dependencies when moving or copying this "package" to another project.
import 'package:komodo_cex_market_data/src/binance/data/binance_provider.dart';
import 'package:komodo_cex_market_data/src/binance/models/binance_exchange_info_reduced.dart';
import 'package:komodo_cex_market_data/src/cex_repository.dart';
import 'package:komodo_cex_market_data/src/models/models.dart';

// Declaring constants here to make this easier to copy & move around
/// The base URL for the Binance API.
String get binanceApiEndpoint => 'https://api.binance.com/api/v3';
List<String> get binanceApiEndpoint =>
['https://api.binance.com/api/v3', 'https://api.binance.us/api/v3'];

BinanceRepository binanceRepository = BinanceRepository(
binanceProvider: const BinanceProvider(),
Expand All @@ -29,31 +31,32 @@ class BinanceRepository implements CexRepository {
return _cachedCoinsList!;
}

final exchangeInfo = await _binanceProvider.fetchExchangeInfoReduced();

final coins = <String, CexCoin>{};
for (final symbol in exchangeInfo.symbols) {
final baseAsset = symbol.baseAsset;
final quoteAsset = symbol.quoteAsset;
try {
return await _executeWithRetry((String baseUrl) async {
final exchangeInfo =
await _binanceProvider.fetchExchangeInfoReduced(baseUrl: baseUrl);
_cachedCoinsList = _convertSymbolsToCoins(exchangeInfo);
return _cachedCoinsList!;
});
} catch (e) {
_cachedCoinsList = List.empty();
}

// TODO(Anon): Decide if this belongs at the repository level considering
// that the repository should provide and transform data as required
// without implementing business logic (or make it an optional parameter).
if (!symbol.isSpotTradingAllowed) {
continue;
}
return _cachedCoinsList!;
}

if (!coins.containsKey(baseAsset)) {
coins[baseAsset] = _binanceCoin(baseAsset, quoteAsset);
} else {
coins[baseAsset] = coins[baseAsset]!.copyWith(
currencies: {...coins[baseAsset]!.currencies, quoteAsset},
);
Future<T> _executeWithRetry<T>(Future<T> Function(String) callback) async {
for (int i = 0; i < binanceApiEndpoint.length; i++) {
try {
return await callback(binanceApiEndpoint.elementAt(i));
} catch (e) {
if (i >= binanceApiEndpoint.length) {
rethrow;
}
}
}

_cachedCoinsList = coins.values.toList();
return _cachedCoinsList!;
throw Exception('Invalid state');
}

CexCoin _binanceCoin(String baseCoinAbbr, String quoteCoinAbbr) {
Expand Down Expand Up @@ -83,13 +86,16 @@ class BinanceRepository implements CexRepository {
final endUnixTimestamp = endAt?.millisecondsSinceEpoch;
final intervalAbbreviation = interval.toAbbreviation();

return _binanceProvider.fetchKlines(
symbol.toString(),
intervalAbbreviation,
startUnixTimestampMilliseconds: startUnixTimestamp,
endUnixTimestampMilliseconds: endUnixTimestamp,
limit: limit,
);
return await _executeWithRetry((String baseUrl) async {
return await _binanceProvider.fetchKlines(
symbol.toString(),
intervalAbbreviation,
startUnixTimestampMilliseconds: startUnixTimestamp,
endUnixTimestampMilliseconds: endUnixTimestamp,
limit: limit,
baseUrl: baseUrl,
);
});
}

@override
Expand Down Expand Up @@ -166,4 +172,30 @@ class BinanceRepository implements CexRepository {

return result;
}

List<CexCoin> _convertSymbolsToCoins(
BinanceExchangeInfoResponseReduced exchangeInfo,
) {
final coins = <String, CexCoin>{};
for (final symbol in exchangeInfo.symbols) {
final baseAsset = symbol.baseAsset;
final quoteAsset = symbol.quoteAsset;

// TODO(Anon): Decide if this belongs at the repository level considering
// that the repository should provide and transform data as required
// without implementing business logic (or make it an optional parameter).
if (!symbol.isSpotTradingAllowed) {
continue;
}

if (!coins.containsKey(baseAsset)) {
coins[baseAsset] = _binanceCoin(baseAsset, quoteAsset);
} else {
coins[baseAsset] = coins[baseAsset]!.copyWith(
currencies: {...coins[baseAsset]!.currencies, quoteAsset},
);
}
}
return coins.values.toList();
}
}

0 comments on commit 78f3f14

Please sign in to comment.