git / moonbite-network / mobile/lib/services/chain_service.dart · raw
import 'dart:convert';import 'package:http/http.dart' as http;import '../models/chain_models.dart';import 'cert_pinning.dart';/// Read/relay client for the MoonBite explorer JSON API.////// This service NEVER sees private keys. The wallet derives keys and signs/// transactions on-device (see `wallet/`); this class only fetches chain state/// and relays already-signed raw transactions.class ChainService { /// Live explorer (demo data until a real moonbited backend is wired up). static const String defaultBaseUrl = 'https://moonbite.org'; String baseUrl; final http.Client _http; final Duration timeout; ChainService({ String? baseUrl, http.Client? httpClient, this.timeout = const Duration(seconds: 15), }) : baseUrl = (baseUrl ?? defaultBaseUrl).replaceAll(RegExp(r'/+$'), ''), // Default to the certificate-pinned client so production traffic can't // be MITM'd via a rogue CA. Tests inject their own mock client. _http = httpClient ?? createPinnedHttpClient() { _requireHttps(this.baseUrl); } static void _requireHttps(String url) { if (Uri.parse(url).scheme != 'https') { throw ArgumentError( 'ChainService requires an https base URL (no cleartext / downgrade); ' 'refusing "$url".', ); } } void setBaseUrl(String url) { final cleaned = url.replaceAll(RegExp(r'/+$'), ''); _requireHttps(cleaned); baseUrl = cleaned; } Uri _u(String path) => Uri.parse('$baseUrl$path'); Map<String, dynamic> _decode(http.Response r) { final body = r.body.isEmpty ? '{}' : r.body; final Map<String, dynamic> json = jsonDecode(body) as Map<String, dynamic>; if (r.statusCode >= 400) { throw ChainApiException( (json['error'] ?? 'HTTP ${r.statusCode}').toString(), r.statusCode, ); } return json; } /// Chain tip + node summary. Future<ChainStatus> getStatus() async { final r = await _http.get(_u('/api/status')).timeout(timeout); return ChainStatus.fromJson(_decode(r)); } /// Spendable outputs for [address] (via server-side scantxoutset). Future<List<Utxo>> getUtxos(String address) async { final r = await _http.get(_u('/api/address/$address/utxos')).timeout(timeout); final json = _decode(r); final list = (json['utxos'] as List?) ?? const []; return list .map((e) => Utxo.fromJson(e as Map<String, dynamic>)) .toList(growable: false); } /// Confirmed / unconfirmed / total balance for [address]. Future<WalletBalance> getBalance(String address) async { final r = await _http.get(_u('/api/address/$address/balance')).timeout(timeout); return WalletBalance.fromJson(_decode(r)); } /// Suggested fee rate in BIG per kB. Future<double> getFeeRate() async { final r = await _http.get(_u('/api/fee')).timeout(timeout); final json = _decode(r); return (json['feerate'] ?? 0.00001).toDouble(); } /// Relays a fully-signed raw transaction; returns the accepted txid. Future<String> broadcast(String rawTxHex) async { final r = await _http .post( _u('/api/tx/broadcast'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'rawtx': rawTxHex}), ) .timeout(timeout); final json = _decode(r); return (json['txid'] ?? '').toString(); } /// Decoded transaction JSON as returned by the node. Future<Map<String, dynamic>> getTransaction(String txid) async { final r = await _http.get(_u('/api/tx/$txid')).timeout(timeout); return _decode(r); }}class ChainApiException implements Exception { final String message; final int statusCode; ChainApiException(this.message, this.statusCode); @override String toString() => 'ChainApiException($statusCode): $message';}