175 lines
5.9 KiB
Dart
175 lines
5.9 KiB
Dart
import 'dart:async';
|
||
|
||
import 'models/coupon_detail.dart';
|
||
import 'models/coupon_holding.dart';
|
||
import 'models/block_info.dart';
|
||
import 'models/transaction_info.dart';
|
||
import 'models/chain_stats.dart';
|
||
import 'models/chain_event.dart';
|
||
import 'models/event_filter.dart';
|
||
import 'models/address_balance.dart';
|
||
import 'rpc/json_rpc_client.dart';
|
||
import 'rpc/websocket_client.dart';
|
||
import 'contracts/contract_abis.dart';
|
||
import 'contracts/contract_addresses.dart';
|
||
import 'utils/formatters.dart';
|
||
|
||
/// Genex Chain SDK 主客户端
|
||
class GenexClient {
|
||
final String rpcUrl;
|
||
final int chainId;
|
||
final JsonRpcClient _rpc;
|
||
GenexWebSocketClient? _ws;
|
||
|
||
GenexClient({
|
||
required this.rpcUrl,
|
||
this.chainId = 8888,
|
||
}) : _rpc = JsonRpcClient(rpcUrl);
|
||
|
||
// ─── 区块查询 ──────────────────────────────────────────
|
||
|
||
/// 获取指定高度区块
|
||
Future<BlockInfo> getBlock(int height) async {
|
||
final result = await _rpc.call(
|
||
'eth_getBlockByNumber',
|
||
[Formatters.toHex(height), false],
|
||
);
|
||
return BlockInfo.fromRpcJson(result as Map<String, dynamic>);
|
||
}
|
||
|
||
/// 获取最新区块
|
||
Future<BlockInfo> getLatestBlock() async {
|
||
final result = await _rpc.call(
|
||
'eth_getBlockByNumber',
|
||
['latest', false],
|
||
);
|
||
return BlockInfo.fromRpcJson(result as Map<String, dynamic>);
|
||
}
|
||
|
||
/// 获取当前区块高度
|
||
Future<int> getBlockHeight() async {
|
||
final result = await _rpc.call('eth_blockNumber', []);
|
||
return Formatters.hexToInt(result as String);
|
||
}
|
||
|
||
// ─── 交易查询 ──────────────────────────────────────────
|
||
|
||
/// 获取交易详情
|
||
Future<TransactionInfo> getTransaction(String txHash) async {
|
||
final txResult = await _rpc.call('eth_getTransactionByHash', [txHash]);
|
||
final receiptResult =
|
||
await _rpc.call('eth_getTransactionReceipt', [txHash]);
|
||
return TransactionInfo.fromRpcJson(
|
||
txResult as Map<String, dynamic>,
|
||
receiptResult as Map<String, dynamic>,
|
||
);
|
||
}
|
||
|
||
// ─── 地址查询 ──────────────────────────────────────────
|
||
|
||
/// 查询地址余额
|
||
Future<AddressBalance> getBalance(String address) async {
|
||
final balanceHex =
|
||
await _rpc.call('eth_getBalance', [address, 'latest']);
|
||
final nonce =
|
||
await _rpc.call('eth_getTransactionCount', [address, 'latest']);
|
||
return AddressBalance(
|
||
address: address,
|
||
balance: Formatters.hexToBigInt(balanceHex as String),
|
||
nonce: Formatters.hexToInt(nonce as String),
|
||
);
|
||
}
|
||
|
||
// ─── 券查询 ────────────────────────────────────────────
|
||
|
||
/// 查询券详情(调用 Coupon.getConfig)
|
||
Future<CouponDetail> getCouponDetail(BigInt tokenId) async {
|
||
final data = ContractAbis.encodeCouponGetConfig(tokenId);
|
||
final result = await _rpc.call('eth_call', [
|
||
{
|
||
'to': ContractAddresses.coupon,
|
||
'data': data,
|
||
},
|
||
'latest',
|
||
]);
|
||
return CouponDetail.fromAbiResult(result as String, tokenId);
|
||
}
|
||
|
||
/// 查询地址持有的券 NFT
|
||
Future<List<CouponHolding>> getCouponHoldings(String address) async {
|
||
// balanceOf
|
||
final balanceData = ContractAbis.encodeBalanceOf(address);
|
||
final balanceHex = await _rpc.call('eth_call', [
|
||
{'to': ContractAddresses.coupon, 'data': balanceData},
|
||
'latest',
|
||
]);
|
||
final balance = Formatters.hexToInt(balanceHex as String);
|
||
|
||
final holdings = <CouponHolding>[];
|
||
for (var i = 0; i < balance; i++) {
|
||
final tokenData =
|
||
ContractAbis.encodeTokenOfOwnerByIndex(address, i);
|
||
final tokenIdHex = await _rpc.call('eth_call', [
|
||
{'to': ContractAddresses.coupon, 'data': tokenData},
|
||
'latest',
|
||
]);
|
||
final tokenId = Formatters.hexToBigInt(tokenIdHex as String);
|
||
holdings.add(CouponHolding(tokenId: tokenId, index: i));
|
||
}
|
||
return holdings;
|
||
}
|
||
|
||
// ─── 链统计 ────────────────────────────────────────────
|
||
|
||
/// 获取链统计信息
|
||
Future<ChainStats> getStats() async {
|
||
final blockHex = await _rpc.call('eth_blockNumber', []);
|
||
final peerCount = await _rpc.call('net_peerCount', []);
|
||
return ChainStats(
|
||
blockHeight: Formatters.hexToInt(blockHex as String),
|
||
peerCount: Formatters.hexToInt(peerCount as String),
|
||
chainId: chainId,
|
||
);
|
||
}
|
||
|
||
// ─── 事件订阅 ──────────────────────────────────────────
|
||
|
||
GenexWebSocketClient _ensureWs() {
|
||
_ws ??= GenexWebSocketClient(rpcUrl.replaceFirst('http', 'ws'));
|
||
return _ws!;
|
||
}
|
||
|
||
/// 统一事件订阅接口(匹配指南 §8.4 subscribeEvents 规范)
|
||
///
|
||
/// [filter] 事件过滤器:
|
||
/// - type='newHeads' 订阅新区块
|
||
/// - type='logs' 订阅合约事件,可指定 address / topics
|
||
Stream<ChainEvent> subscribeEvents(EventFilter filter) {
|
||
return _ensureWs().subscribe(filter.type, filter.params);
|
||
}
|
||
|
||
/// 连接 WebSocket 并订阅新区块头
|
||
Stream<ChainEvent> subscribeNewHeads() {
|
||
return _ensureWs().subscribe('newHeads', {});
|
||
}
|
||
|
||
/// 订阅合约事件日志
|
||
Stream<ChainEvent> subscribeLogs({
|
||
String? address,
|
||
List<String>? topics,
|
||
}) {
|
||
final params = <String, dynamic>{};
|
||
if (address != null) params['address'] = address;
|
||
if (topics != null) params['topics'] = topics;
|
||
return _ensureWs().subscribe('logs', params);
|
||
}
|
||
|
||
// ─── 生命周期 ──────────────────────────────────────────
|
||
|
||
/// 关闭客户端连接
|
||
void close() {
|
||
_rpc.close();
|
||
_ws?.close();
|
||
}
|
||
}
|