staking_service.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import 'package:dio/dio.dart';
  2. import 'package:flutter/foundation.dart';
  3. import '../../core/utils/spot_transfer_asset.dart';
  4. import '../models/finance/airdrop_eligibility.dart';
  5. import '../models/finance/airdrop_record_item.dart';
  6. import '../models/finance/staking_config.dart';
  7. import '../models/finance/staking_order_item.dart';
  8. import '../models/finance/staking_wallet_balance.dart';
  9. class StakingService {
  10. const StakingService(this._dio);
  11. final Dio _dio;
  12. void _log(String message) {
  13. if (kDebugMode) {
  14. debugPrint('[Airdrop][Service] $message');
  15. }
  16. }
  17. /// 与 Web `STAKING_COIN_UNIT` / `stakingCoinUnit()` 默认币种一致
  18. static const stakingCoinUnit = 'IBIT';
  19. Future<List<StakingConfig>> getConfigs() async {
  20. final resp =
  21. await _dio.post<Map<String, dynamic>>('uc/staking/config/list');
  22. final raw = resp.data?['data'];
  23. if (raw is List) {
  24. return raw
  25. .whereType<Map<String, dynamic>>()
  26. .map(StakingConfig.fromJson)
  27. .toList();
  28. }
  29. return const [];
  30. }
  31. Future<StakingConfig?> getMinOpenConfig() async {
  32. final resp =
  33. await _dio.post<Map<String, dynamic>>('uc/staking/config/min-open');
  34. final raw = resp.data?['data'];
  35. if (raw is Map<String, dynamic>) {
  36. return StakingConfig.fromJson(raw);
  37. }
  38. return null;
  39. }
  40. Future<void> submitStake({
  41. required int configId,
  42. required String amount,
  43. }) async {
  44. await _dio.post<dynamic>(
  45. 'uc/staking/stake',
  46. data: {'configId': configId, 'amount': amount},
  47. options: Options(contentType: Headers.formUrlEncodedContentType),
  48. );
  49. }
  50. /// 分页查询我的质押订单(对齐 Web fetchStakingOrders)
  51. Future<StakingOrderPage> getStakingOrders({
  52. required int pageNo,
  53. int pageSize = 5,
  54. String? status,
  55. }) async {
  56. final data = <String, dynamic>{
  57. 'pageNo': pageNo,
  58. 'pageSize': pageSize,
  59. };
  60. if (status != null && status.isNotEmpty) {
  61. data['status'] = status;
  62. }
  63. final resp = await _dio.post<Map<String, dynamic>>(
  64. 'uc/staking/orders',
  65. data: data,
  66. options: Options(contentType: Headers.formUrlEncodedContentType),
  67. );
  68. final raw = resp.data?['data'];
  69. if (raw is! Map<String, dynamic>) {
  70. return const StakingOrderPage(content: [], totalPages: 0, pageNo: 1);
  71. }
  72. final contentRaw = raw['content'];
  73. final content = contentRaw is List
  74. ? contentRaw
  75. .whereType<Map<String, dynamic>>()
  76. .map(StakingOrderItem.fromJson)
  77. .toList()
  78. : <StakingOrderItem>[];
  79. final totalPages = int.tryParse('${raw['totalPages'] ?? 0}') ?? 0;
  80. final rawPageNo = int.tryParse('${raw['pageNo'] ?? ''}');
  81. final rawNumber = int.tryParse('${raw['number'] ?? ''}');
  82. final parsedPageNo = rawPageNo ?? ((rawNumber ?? (pageNo - 1)) + 1);
  83. return StakingOrderPage(
  84. content: content,
  85. totalPages: totalPages,
  86. pageNo: parsedPageNo,
  87. );
  88. }
  89. Future<StakingWalletBalance> getStakingWalletBalance(String coinUnit) async {
  90. final resp = await _dio.post<Map<String, dynamic>>(
  91. 'uc/staking/wallet',
  92. data: {'coinUnit': coinUnit},
  93. options: Options(contentType: Headers.formUrlEncodedContentType),
  94. );
  95. final raw = resp.data?['data'];
  96. if (raw is Map<String, dynamic>) {
  97. return StakingWalletBalance.fromJson(raw);
  98. }
  99. return StakingWalletBalance.empty(coinUnit);
  100. }
  101. /// 可提现账户指定币种可用余额(对齐 Web `loadFundCoinBalance`)
  102. ///
  103. /// POST `/uc/asset/wallet`,解析逻辑同 `parseFundWalletBalances`。
  104. Future<String> getFundingWalletAvailable(String coinUnit) async {
  105. try {
  106. final resp = await _dio.post<Map<String, dynamic>>(
  107. 'uc/asset/wallet',
  108. data: <String, dynamic>{},
  109. );
  110. final map = parseFundWalletBalances(resp.data);
  111. return map[coinUnit.toUpperCase()] ?? '0';
  112. } catch (_) {
  113. return '0';
  114. }
  115. }
  116. Future<AirdropEligibility> getAirdropEligibility() async {
  117. final resp =
  118. await _dio.post<Map<String, dynamic>>('uc/airdrop/eligibility');
  119. final raw = resp.data?['data'];
  120. _log(
  121. 'eligibility response dataType=${raw.runtimeType} keys=${raw is Map<String, dynamic> ? raw.keys.toList() : 'n/a'}',
  122. );
  123. if (raw is Map<String, dynamic>) {
  124. return AirdropEligibility.fromJson(raw);
  125. }
  126. _log('eligibility fallback to empty because data is not map');
  127. return AirdropEligibility.empty();
  128. }
  129. Future<void> claimAirdrop() async {
  130. await _dio.post<Map<String, dynamic>>('uc/airdrop/claim');
  131. }
  132. Future<AirdropRecordPage> getAirdropRecords({
  133. required int pageNo,
  134. int pageSize = 10,
  135. }) async {
  136. final resp = await _dio.post<Map<String, dynamic>>(
  137. 'uc/airdrop/records',
  138. data: {'pageNo': pageNo, 'pageSize': pageSize},
  139. options: Options(contentType: Headers.formUrlEncodedContentType),
  140. );
  141. final raw = resp.data?['data'];
  142. _log(
  143. 'records response pageNo=$pageNo pageSize=$pageSize dataType=${raw.runtimeType}');
  144. if (raw is! Map<String, dynamic>) {
  145. _log('records fallback empty: data is not map');
  146. return const AirdropRecordPage(content: [], totalPages: 0, pageNo: 1);
  147. }
  148. final contentRaw = raw['content'];
  149. final content = contentRaw is List
  150. ? contentRaw
  151. .whereType<Map<String, dynamic>>()
  152. .map(AirdropRecordItem.fromJson)
  153. .toList()
  154. : <AirdropRecordItem>[];
  155. final totalPages = int.tryParse('${raw['totalPages'] ?? 0}') ?? 0;
  156. // 后端常见两种分页字段:
  157. // 1) pageNo: 1 基页码
  158. // 2) number: 0 基页码(Spring Pageable)
  159. final rawPageNo = int.tryParse('${raw['pageNo'] ?? ''}');
  160. final rawNumber = int.tryParse('${raw['number'] ?? ''}');
  161. final parsedPageNo = rawPageNo ?? ((rawNumber ?? (pageNo - 1)) + 1);
  162. _log(
  163. 'records parsed pageNo=$parsedPageNo(totalPages=$totalPages, rawPageNo=$rawPageNo, rawNumber=$rawNumber) contentRawType=${contentRaw.runtimeType} contentCount=${content.length}',
  164. );
  165. return AirdropRecordPage(
  166. content: content,
  167. totalPages: totalPages,
  168. pageNo: parsedPageNo,
  169. );
  170. }
  171. }