requestFaucetV3 function

Future<FaucetV3Response> requestFaucetV3(
  1. String endpoint,
  2. String address, {
  3. Dio? dio,
  4. CancelToken? cancelToken,
  5. Duration timeout = const Duration(minutes: 3),
  6. Map<String, String> headers = const {},
})

Fetches a challenge, solves it and submits exactly one payout request.

Implementation

Future<FaucetV3Response> requestFaucetV3(
  String endpoint,
  String address, {
  Dio? dio,
  CancelToken? cancelToken,
  Duration timeout = const Duration(minutes: 3),
  Map<String, String> headers = const {},
}) async {
  if (address.replaceFirst(RegExp(r'^0x', caseSensitive: false), '').isEmpty) {
    throw ArgumentError.value(address, 'recipient');
  }
  final recipient = normalizeSuiAddress(address);
  if (!isValidSuiAddress(recipient)) {
    throw ArgumentError.value(address, 'recipient');
  }
  if (timeout <= Duration.zero) throw ArgumentError.value(timeout, 'timeout');
  final http = dio ?? Dio();
  final ownsClient = dio == null;
  final token = CancelToken();
  final started = Stopwatch()..start();
  final timer = Timer(
    timeout,
    () => token.cancel(TimeoutException('Faucet request timed out', timeout)),
  );
  if (cancelToken?.isCancelled ?? false) token.cancel(cancelToken!.cancelError);
  unawaited(cancelToken?.whenCancel.then(token.cancel) ?? Future<void>.value());
  void checkCancelled() {
    if (token.isCancelled) {
      final reason = token.cancelError?.error;
      if (reason is TimeoutException) throw reason;
      throw token.cancelError!;
    }
  }

  Future<Map<String, dynamic>> request(
    String path, [
    Map<String, dynamic>? body,
  ]) async {
    checkCancelled();
    final Response<dynamic> response;
    try {
      response = await http.request<dynamic>(
        Uri.parse(endpoint).resolve(path).toString(),
        data: body,
        cancelToken: token,
        options: Options(
          method: body == null ? 'GET' : 'POST',
          headers: headers,
          contentType: 'application/json',
          validateStatus: (_) => true,
        ),
      );
    } on DioException {
      checkCancelled();
      rethrow;
    }
    checkCancelled();
    final data = response.data;
    if ((response.statusCode ?? 500) >= 400) {
      throw FaucetException(
        data is Map && data['error'] is String
            ? data['error'] as String
            : 'Faucet request failed',
        status: response.statusCode ?? 500,
        code: data is Map && data['code'] is String
            ? data['code'] as String
            : null,
        digest: data is Map && data['digest'] is String
            ? data['digest'] as String
            : null,
      );
    }
    if (data is! Map) {
      throw const FormatException('Invalid faucet JSON response');
    }
    return Map<String, dynamic>.from(data);
  }

  try {
    final random = Random.secure();
    while (true) {
      final challengeStart = started.elapsed;
      final challenge = FaucetChallenge.fromJson(
        await request('/v3/challenge?recipient=$recipient'),
      );
      if (challenge.recipient != recipient) {
        throw const FormatException('Faucet challenge recipient mismatch');
      }
      final deadline =
          challengeStart +
          Duration(milliseconds: challenge.windowSeconds * 800);
      var nonce =
          (BigInt.from(random.nextInt(0x100000000)) << 32) |
          BigInt.from(random.nextInt(0x100000000));
      while (started.elapsed < deadline) {
        checkCancelled();
        final hash = await Future.any([
          challenge.hashProof(nonce),
          token.whenCancel.then<Uint8List>((_) {
            checkCancelled();
            throw StateError('Cancelled');
          }),
        ]);
        await Future<void>.delayed(Duration.zero);
        checkCancelled();
        if (started.elapsed >= deadline) break;
        final first = hash
            .take(8)
            .fold(
              BigInt.zero,
              (BigInt value, byte) => (value << 8) | BigInt.from(byte),
            );
        if (first < challenge.threshold) {
          final result = FaucetV3Response.fromJson(
            await request('/v3/gas', {
              'recipient': recipient,
              'checkpointSeq': challenge.checkpointSeq,
              'nonce': nonce.toString(),
              'hashHex': hexEncode(hash),
            }),
          );
          if (result.recipient != recipient) {
            throw const FormatException('Faucet response recipient mismatch');
          }
          return result;
        }
        nonce = (nonce + BigInt.one) % _u64Limit;
      }
    }
  } finally {
    timer.cancel();
    started.stop();
    if (ownsClient) http.close();
  }
}