Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
481 changes: 304 additions & 177 deletions example/testnet_integration_example.dart

Large diffs are not rendered by default.

124 changes: 104 additions & 20 deletions lib/src/babel_binance_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,45 @@ import './testnet.dart';
import './config/binance_config.dart';
import './logging/logger.dart';

/// Main entry point for the Binance API wrapper.
///
/// Provides access to all Binance API endpoints including:
/// - Spot trading (production)
/// - Futures USD-M trading (production)
/// - Margin trading (production)
/// - Testnet APIs (testnet.binance.vision)
/// - Demo Trading APIs (demo-api.binance.com)
///
/// Example usage:
/// ```dart
/// // Production trading
/// final binance = Binance(
/// apiKey: 'your-api-key',
/// apiSecret: 'your-api-secret',
/// );
///
/// // Testnet trading
/// final testnet = Binance.testnet(
/// apiKey: 'testnet-api-key',
/// apiSecret: 'testnet-api-secret',
/// );
///
/// // Demo trading (alternative testnet)
/// final demo = Binance.demo(
/// apiKey: 'demo-api-key',
/// apiSecret: 'demo-api-secret',
/// );
/// ```
class Binance {
final Spot spot;
final SimulatedConvert simulatedConvert;
final FuturesUsd futuresUsd;
final Margin margin;
final TestnetSpot testnetSpot;
final TestnetFuturesUsd testnetFutures;
final TestnetFuturesCoinM testnetFuturesCoinM;
final DemoSpot demoSpot;
final DemoFuturesUsd demoFutures;
final BinanceConfig config;
final BinanceLogger logger;

Expand All @@ -21,21 +53,27 @@ class Binance {
String? apiSecret,
BinanceConfig? config,
BinanceLogger? logger,
}) : config = config ?? BinanceConfig.defaultConfig,
logger = logger ?? const NoOpLogger(),
spot = Spot(apiKey: apiKey, apiSecret: apiSecret),
simulatedConvert =
SimulatedConvert(apiKey: apiKey, apiSecret: apiSecret),
futuresUsd = FuturesUsd(apiKey: apiKey, apiSecret: apiSecret),
margin = Margin(apiKey: apiKey, apiSecret: apiSecret),
testnetSpot = TestnetSpot(apiKey: apiKey, apiSecret: apiSecret),
testnetFutures =
TestnetFuturesUsd(apiKey: apiKey, apiSecret: apiSecret);
}) : config = config ?? BinanceConfig.defaultConfig,
logger = logger ?? const NoOpLogger(),
spot = Spot(apiKey: apiKey, apiSecret: apiSecret),
simulatedConvert =
SimulatedConvert(apiKey: apiKey, apiSecret: apiSecret),
futuresUsd = FuturesUsd(apiKey: apiKey, apiSecret: apiSecret),
margin = Margin(apiKey: apiKey, apiSecret: apiSecret),
testnetSpot = TestnetSpot(apiKey: apiKey, apiSecret: apiSecret),
testnetFutures = TestnetFuturesUsd(apiKey: apiKey, apiSecret: apiSecret),
testnetFuturesCoinM = TestnetFuturesCoinM(apiKey: apiKey, apiSecret: apiSecret),
demoSpot = DemoSpot(apiKey: apiKey, apiSecret: apiSecret),
demoFutures = DemoFuturesUsd(apiKey: apiKey, apiSecret: apiSecret);

/// Create a Binance instance specifically configured for testnet
///
/// Use this when you want to test with real API endpoints but test data
/// Use this when you want to test with real API endpoints but test data.
/// Get your testnet API keys from: https://testnet.binance.vision/
///
/// Available endpoints:
/// - REST API: https://testnet.binance.vision/api
/// - WebSocket: wss://stream.testnet.binance.vision:9443
factory Binance.testnet({
required String apiKey,
required String apiSecret,
Expand All @@ -50,17 +88,63 @@ class Binance {
);
}

/// Dispose and clean up resources
void dispose() {
/// Create a Binance instance specifically configured for Demo Trading
///
/// Use this when testnet.binance.vision is not accessible in your region.
/// Get your demo API keys from your Binance account settings.
///
/// Available endpoints:
/// - REST API: https://demo-api.binance.com
/// - WebSocket: wss://demo-stream.binance.com
factory Binance.demo({
required String apiKey,
required String apiSecret,
BinanceConfig? config,
BinanceLogger? logger,
}) {
return Binance(
apiKey: apiKey,
apiSecret: apiSecret,
config: config,
logger: logger,
);
}

/// Dispose and clean up all resources
///
/// Call this when you're done using the Binance client to properly
/// close all HTTP connections, WebSocket connections, and release resources.
Future<void> dispose() async {
// Dispose spot trading resources
spot.market.dispose();
spot.trading.dispose();
spot.userDataStream.dispose();

// Dispose simulated convert
simulatedConvert.dispose();

// Dispose futures resources
futuresUsd.dispose();

// Dispose margin resources
margin.dispose();
// Note: TestnetSpot and TestnetFuturesUsd don't have dispose methods
// as they are composed of sub-classes that handle their own cleanup
}
}

/// Checks if you are awesome. Spoiler: you are.
class Awesome {
bool get isAwesome => true;
// Dispose testnet resources (async due to WebSocket)
await testnetSpot.dispose();

// Dispose testnet futures
testnetFutures.market.dispose();
testnetFutures.trading.dispose();

// Dispose testnet COIN-M futures
testnetFuturesCoinM.market.dispose();
testnetFuturesCoinM.trading.dispose();

// Dispose demo resources (async due to WebSocket)
await demoSpot.dispose();

// Dispose demo futures
demoFutures.market.dispose();
demoFutures.trading.dispose();
}
}
112 changes: 91 additions & 21 deletions lib/src/binance_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import 'exceptions/network_exception.dart';
import 'exceptions/validation_exception.dart';

class BinanceBase {
final String? apiKey;
final String? apiSecret;
// Private credentials for security
final String? _apiKey;
final String? _apiSecret;
final String baseUrl;
final BinanceConfig config;
final BinanceLogger logger;
Expand All @@ -27,30 +28,48 @@ class BinanceBase {
int _serverTimeOffset = 0;
DateTime? _lastServerTimeSync;

// Lock for server time sync to prevent race conditions
Completer<void>? _serverTimeSyncLock;

/// Returns true if API credentials are configured
bool get hasCredentials => _apiKey != null && _apiSecret != null;

/// Returns the API key (for headers) - null if not configured
String? get apiKey => _apiKey;

BinanceBase({
this.apiKey,
this.apiSecret,
String? apiKey,
String? apiSecret,
required this.baseUrl,
BinanceConfig? config,
BinanceLogger? logger,
}) : config = config ?? BinanceConfig.defaultConfig,
logger = logger ?? const NoOpLogger(),
_endpoints = _generateEndpoints(baseUrl) {

}) : _apiKey = apiKey,
_apiSecret = apiSecret,
config = config ?? BinanceConfig.defaultConfig,
logger = logger ?? const NoOpLogger(),
_endpoints = _generateEndpoints(baseUrl) {
rateLimiter = RateLimiter(
config: this.config.rateLimitConfig,
);

_httpClient = BinanceHttpClient(config: this.config);

// Sync server time if enabled
if (this.config.syncServerTime && apiSecret != null) {
if (this.config.syncServerTime && _apiSecret != null) {
_initServerTimeSync();
}
}

/// Initialize server time synchronization
/// Initialize server time synchronization with locking
Future<void> _initServerTimeSync() async {
// If sync already in progress, wait for it
if (_serverTimeSyncLock != null) {
await _serverTimeSyncLock!.future;
return;
}

_serverTimeSyncLock = Completer<void>();

try {
final serverTimeData = await _getServerTimeInternal();
final serverTime = serverTimeData['serverTime'] as int;
Expand All @@ -63,6 +82,9 @@ class BinanceBase {
} catch (e) {
logger.warn('Failed to sync server time', error: e);
// Continue anyway, server time sync is optional
} finally {
_serverTimeSyncLock?.complete();
_serverTimeSyncLock = null;
}
}

Expand All @@ -85,7 +107,7 @@ class BinanceBase {

/// Re-sync server time if needed (every 30 minutes)
Future<void> _resyncServerTimeIfNeeded() async {
if (!config.syncServerTime || apiSecret == null) return;
if (!config.syncServerTime || _apiSecret == null) return;

if (_lastServerTimeSync == null ||
DateTime.now().difference(_lastServerTimeSync!) >
Expand Down Expand Up @@ -131,6 +153,14 @@ class BinanceBase {
}
}

// Testnet Spot API endpoints with failover
if (host == 'testnet.binance.vision') {
return [
'$scheme://testnet.binance.vision$port$path',
'$scheme://api1.testnet.binance.vision$port$path',
];
}

// For non-Binance domains or unrecognized patterns, return original URL
return [baseUrl];
}
Expand Down Expand Up @@ -186,14 +216,17 @@ class BinanceBase {
params ??= {};

// Add signature if authenticated
if (apiSecret != null) {
if (_apiSecret != null) {
params['timestamp'] = _getSyncedTimestamp();
params['recvWindow'] = config.recvWindow;

final query = Uri(queryParameters: params.map((key, value) =>
MapEntry(key, value.toString()))).query;
final signature = Hmac(sha256, utf8.encode(apiSecret!))
.convert(utf8.encode(query)).toString();
final query = Uri(
queryParameters:
params.map((key, value) => MapEntry(key, value.toString())))
.query;
final signature = Hmac(sha256, utf8.encode(_apiSecret!))
.convert(utf8.encode(query))
.toString();
params['signature'] = signature;
}

Expand Down Expand Up @@ -277,12 +310,49 @@ class BinanceBase {
_resetToPrimaryEndpoint();
}

return json.decode(response.body);
// Parse response JSON safely
dynamic responseData;
try {
responseData = json.decode(response.body);
} on FormatException catch (e) {
throw BinanceApiException(
statusCode: response.statusCode,
errorMessage: 'Invalid JSON response: ${e.message}',
responseBody: {'raw_body': response.body.length > 500
? '${response.body.substring(0, 500)}...'
: response.body},
);
}
return responseData;
} else {
// Parse error response
final errorBody = json.decode(response.body) as Map<String, dynamic>?;
final errorCode = errorBody?['code'] as int?;
final errorMsg = errorBody?['msg'] as String?;
// Parse error response safely
Map<String, dynamic>? errorBody;
int? errorCode;
String? errorMsg;

try {
final decoded = json.decode(response.body);
if (decoded is Map<String, dynamic>) {
errorBody = decoded;
// Handle both int and string error codes
final rawCode = errorBody['code'];
errorCode = rawCode is int
? rawCode
: (rawCode is String ? int.tryParse(rawCode) : null);
errorMsg = errorBody['msg']?.toString();
}
} on FormatException {
// Non-JSON response (WAF, Cloudflare, HTML error pages)
errorMsg = 'HTTP ${response.statusCode}: Non-JSON response';
if (response.body.trimLeft().startsWith('<')) {
errorMsg = 'Blocked by WAF/CDN (${response.statusCode})';
}
errorBody = {
'raw_body': response.body.length > 500
? '${response.body.substring(0, 500)}...'
: response.body
};
}

// Log error response
logger.logResponse(
Expand Down
Loading