-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Practical, copyable patterns for the cryptohopper Dart package. Every snippet runs as-is — drop into a Dart file and execute with dart run file.dart. They use only the public package surface, never internals.
The SDK is async: every resource method returns Future<dynamic> (decoded JSON). Use await everywhere; mix try / on CryptohopperException catch (e) to handle errors.
Until the package is on pub.dev (see PUBLISHING.md) install from git:
# pubspec.yaml
dependencies:
cryptohopper:
git:
url: https://github.com/cryptohopper/cryptohopper-dart-sdk
ref: v0.1.0-alpha.1- Build a client and clean up
- Wait for a backtest to finish
- Find every open position across all your hoppers
- Detect new fills with Timer.periodic
- Switch on CryptohopperException codes (Dart 3)
- Fail fast on auth errors, retry on transient ones
- Run multiple SDK calls in parallel with Future.wait
- Bring your own http.Client (instrumentation, mTLS)
- Tighten timeouts for short-lived workers and Flutter
- Disable the SDK's built-in retry and handle 429 yourself
- Mock the SDK in tests with package:http MockClient
The client owns an http.Client connection pool. Call close() when you're done — leaking pools holds open file descriptors.
import 'dart:io';
import 'package:cryptohopper/cryptohopper.dart';
Future<void> main() async {
final client = CryptohopperClient(
apiKey: Platform.environment['CRYPTOHOPPER_TOKEN']!,
);
try {
final me = await client.user.get();
print(me['email']);
} finally {
client.close();
}
}Inside Flutter, instantiate once at app startup and dispose with the rest of your services.
Backtests run async server-side. create returns immediately with an ID; you poll get until status is terminal.
Future<Map<String, dynamic>> runBacktest(
CryptohopperClient client, {
required Object hopperId,
required String fromDate,
required String toDate,
}) async {
final submitted = await client.backtest.create({
'hopper_id': hopperId,
'start_date': fromDate,
'end_date': toDate,
}) as Map<String, dynamic>;
while (true) {
final cur = await client.backtest.get(submitted['id']) as Map<String, dynamic>;
if (cur['status'] == 'completed' || cur['status'] == 'failed') {
return cur;
}
await Future<void>.delayed(const Duration(seconds: 5));
}
}The backtest rate bucket is separate (1 request per 2 seconds). 5-second polling stays well clear.
final hoppers = await client.hoppers.list() as List;
for (final h in hoppers) {
final positions = await client.hoppers.positions(h['id']) as List;
for (final p in positions) {
print('${h['name']} (#${h['id']}): ${p['amount']} ${p['coin']} @ ${p['rate']}');
}
}Sequential. With many hoppers, parallelise with Future.wait (recipe below).
import 'dart:async';
final seen = <Object>{};
Timer.periodic(const Duration(seconds: 10), (_) async {
try {
final orders = await client.hoppers.orders(hopperId) as List;
for (final o in orders) {
final id = o['id'];
if (id == null || seen.contains(id)) continue;
if (o['status'] == 'filled') {
seen.add(id);
print('Fill: ${o['market']} ${o['type']} ${o['amount']} @ ${o['price']}');
}
}
} on CryptohopperException catch (e) {
print('poll error: ${e.code}');
}
});For production-grade fill notifications, configure the webhooks resource — push beats poll.
Dart 3's switch expression makes the typed-error surface neat.
try {
await client.hoppers.get('999999999');
} on CryptohopperException catch (e) {
final action = switch (e.code) {
'NOT_FOUND' => 'no such hopper',
'UNAUTHORIZED' || 'FORBIDDEN' => 'auth problem; IP we sent: ${e.ipAddress ?? "unknown"}',
'RATE_LIMITED' => 'rate limited; retry after ${e.retryAfterMs}ms',
_ => null,
};
if (action == null) rethrow;
print(action);
}e.code is a stable string — compare with ==, never substring-match.
The SDK auto-retries 429s. For 5xx and network errors you may want a tighter retry. Auth errors should never be retried.
Future<T> withRetry<T>(Future<T> Function() fn, {int maxAttempts = 3}) async {
for (var attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} on CryptohopperException catch (e) {
const fatal = {'UNAUTHORIZED', 'FORBIDDEN', 'NOT_FOUND', 'VALIDATION_ERROR'};
if (fatal.contains(e.code)) rethrow;
if (attempt + 1 == maxAttempts) rethrow;
await Future<void>.delayed(Duration(milliseconds: 500 * (1 << attempt)));
}
}
throw StateError('unreachable');
}
final me = await withRetry(() => client.user.get());final hoppers = await client.hoppers.list() as List;
// Issue all positions calls concurrently. The SDK is reentrant — share one
// client across awaits.
final futures = hoppers.map<Future<MapEntry<Object, List>>>((h) async {
final positions = await client.hoppers.positions(h['id']) as List;
return MapEntry(h['id'] as Object, positions);
});
final results = await Future.wait(futures);
for (final r in results) {
print('hopper ${r.key}: ${r.value.length} positions');
}Each in-flight call counts against the normal bucket (30 req/min). With 50+ hoppers, batch — Future.wait doesn't bound concurrency on its own:
Future<List<R>> mapConcurrent<T, R>(Iterable<T> items, int limit, Future<R> Function(T) fn) async {
final out = <R>[];
final list = items.toList();
for (var i = 0; i < list.length; i += limit) {
final chunk = list.skip(i).take(limit);
out.addAll(await Future.wait(chunk.map(fn)));
}
return out;
}import 'package:http/http.dart' as http;
class LoggingClient extends http.BaseClient {
final http.Client _inner;
LoggingClient(this._inner);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final start = DateTime.now();
final res = await _inner.send(request);
print('${request.method} ${request.url} -> ${res.statusCode} '
'(${DateTime.now().difference(start).inMilliseconds}ms)');
return res;
}
@override
void close() => _inner.close();
}
final client = CryptohopperClient(
apiKey: token,
httpClient: LoggingClient(http.Client()),
);When you pass httpClient, you're responsible for closing it — client.close() won't touch a client you supplied.
Default timeout is 30 seconds. In a Flutter splash screen or a Cloud Functions invocation, that's too long.
final client = CryptohopperClient(
apiKey: token,
timeout: const Duration(seconds: 8), // ~half your budget
maxRetries: 1, // leave room for one retry
);A CryptohopperException with code == "TIMEOUT" is much easier to handle than a hung UI.
final client = CryptohopperClient(
apiKey: token,
maxRetries: 0,
);
try {
await client.hoppers.list();
} on CryptohopperException catch (e) {
if (e.code == 'RATE_LIMITED') {
print('rate limited; server says wait ${e.retryAfterMs}ms');
// your custom queue / circuit breaker / etc.
} else {
rethrow;
}
}Useful when you have your own queue, want exact backoff control, or are running inside a workflow engine that already does retries.
The SDK accepts any http.Client. package:http's MockClient is the standard Dart way to test HTTP code.
import 'package:cryptohopper/cryptohopper.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:test/test.dart';
void main() {
test('user.get returns the parsed payload', () async {
final mock = MockClient((req) async {
expect(req.method, 'GET');
expect(req.url.path, '/v1/user/get');
return http.Response('{"data":{"id":42,"email":"alice@example.com"}}', 200);
});
final client = CryptohopperClient(apiKey: 'test', httpClient: mock);
addTearDown(() => client.close());
final me = await client.user.get();
expect(me['id'], 42);
});
test('retries on 429 then succeeds', () async {
var calls = 0;
final mock = MockClient((req) async {
calls++;
if (calls == 1) {
return http.Response('{}', 429, headers: {'retry-after': '0'});
}
return http.Response('{"data":{"id":42}}', 200);
});
final client = CryptohopperClient(apiKey: 'test', httpClient: mock);
addTearDown(() => client.close());
final me = await client.user.get();
expect(me['id'], 42);
expect(calls, 2);
});
}The SDK pulls data out of the envelope automatically — your mock returns {"data": ...}, your assertion sees the inner value.
Pages
Other SDKs
Resources
- Source code
- pub.dev (pending bootstrap)
- Public API docs
- Versioning policy