staking_service.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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_team_overview.dart';
  9. import '../models/finance/staking_wallet_balance.dart';
  10. class StakingService {
  11. const StakingService(this._dio);
  12. final Dio _dio;
  13. void _log(String message) {
  14. if (kDebugMode) {
  15. debugPrint('[Airdrop][Service] $message');
  16. }
  17. }
  18. /// 与 Web `STAKING_COIN_UNIT` / `stakingCoinUnit()` 默认币种一致
  19. static const stakingCoinUnit = 'IBIT';
  20. Future<List<StakingConfig>> getConfigs() async {
  21. final resp =
  22. await _dio.post<Map<String, dynamic>>('uc/staking/config/list');
  23. final raw = resp.data?['data'];
  24. if (raw is List) {
  25. return raw
  26. .whereType<Map<String, dynamic>>()
  27. .map(StakingConfig.fromJson)
  28. .toList();
  29. }
  30. return const [];
  31. }
  32. Future<StakingConfig?> getMinOpenConfig() async {
  33. final resp =
  34. await _dio.post<Map<String, dynamic>>('uc/staking/config/min-open');
  35. final raw = resp.data?['data'];
  36. if (raw is Map<String, dynamic>) {
  37. return StakingConfig.fromJson(raw);
  38. }
  39. return null;
  40. }
  41. Future<void> submitStake({
  42. required int configId,
  43. required String amount,
  44. }) async {
  45. await _dio.post<dynamic>(
  46. 'uc/staking/stake',
  47. data: {'configId': configId, 'amount': amount},
  48. options: Options(contentType: Headers.formUrlEncodedContentType),
  49. );
  50. }
  51. /// 分页查询我的质押订单(对齐 Web fetchStakingOrders)
  52. Future<StakingOrderPage> getStakingOrders({
  53. required int pageNo,
  54. int pageSize = 5,
  55. String? status,
  56. }) async {
  57. final data = <String, dynamic>{
  58. 'pageNo': pageNo,
  59. 'pageSize': pageSize,
  60. };
  61. if (status != null && status.isNotEmpty) {
  62. data['status'] = status;
  63. }
  64. final resp = await _dio.post<Map<String, dynamic>>(
  65. 'uc/staking/orders',
  66. data: data,
  67. options: Options(contentType: Headers.formUrlEncodedContentType),
  68. );
  69. final raw = resp.data?['data'];
  70. if (raw is! Map<String, dynamic>) {
  71. return const StakingOrderPage(content: [], totalPages: 0, pageNo: 1);
  72. }
  73. final contentRaw = raw['content'];
  74. final content = contentRaw is List
  75. ? contentRaw
  76. .whereType<Map<String, dynamic>>()
  77. .map(StakingOrderItem.fromJson)
  78. .toList()
  79. : <StakingOrderItem>[];
  80. final totalPages = int.tryParse('${raw['totalPages'] ?? 0}') ?? 0;
  81. final rawPageNo = int.tryParse('${raw['pageNo'] ?? ''}');
  82. final rawNumber = int.tryParse('${raw['number'] ?? ''}');
  83. final parsedPageNo = rawPageNo ?? ((rawNumber ?? (pageNo - 1)) + 1);
  84. return StakingOrderPage(
  85. content: content,
  86. totalPages: totalPages,
  87. pageNo: parsedPageNo,
  88. );
  89. }
  90. /// 查询质押团队总览(对齐 Web `fetchStakingTeamOverview`)
  91. Future<StakingTeamOverview> getTeamOverview() async {
  92. final resp = await _dio.post<Map<String, dynamic>>(
  93. 'uc/staking/team',
  94. data: <String, dynamic>{},
  95. options: Options(contentType: Headers.formUrlEncodedContentType),
  96. );
  97. final raw = resp.data?['data'];
  98. if (raw is Map<String, dynamic>) {
  99. return StakingTeamOverview.fromJson(raw);
  100. }
  101. return const StakingTeamOverview();
  102. }
  103. /// 分页查询直推列表(对齐 Web `fetchStakingDirectList`)
  104. Future<StakingDirectListPage> getDirectList({
  105. int pageNo = 1,
  106. int pageSize = 20,
  107. }) async {
  108. final resp = await _dio.post<Map<String, dynamic>>(
  109. 'uc/staking/direct/list',
  110. data: {'pageNo': pageNo, 'pageSize': pageSize},
  111. options: Options(contentType: Headers.formUrlEncodedContentType),
  112. );
  113. final raw = resp.data?['data'];
  114. if (raw is! Map<String, dynamic>) {
  115. return const StakingDirectListPage(
  116. content: [],
  117. totalPages: 0,
  118. pageNo: 1,
  119. hasMore: false,
  120. );
  121. }
  122. final contentRaw = raw['content'];
  123. final content = contentRaw is List
  124. ? contentRaw
  125. .whereType<Map<String, dynamic>>()
  126. .map(StakingDirectMember.fromJson)
  127. .toList()
  128. : <StakingDirectMember>[];
  129. final totalPages = int.tryParse('${raw['totalPages'] ?? 0}') ?? 0;
  130. final rawPageNo = int.tryParse('${raw['pageNo'] ?? ''}');
  131. final rawNumber = int.tryParse('${raw['number'] ?? ''}');
  132. final parsedPageNo = rawPageNo ?? ((rawNumber ?? (pageNo - 1)) + 1);
  133. final last = raw['last'];
  134. final hasMore = last is bool ? !last : parsedPageNo < totalPages;
  135. return StakingDirectListPage(
  136. content: content,
  137. totalPages: totalPages,
  138. pageNo: parsedPageNo,
  139. hasMore: hasMore,
  140. );
  141. }
  142. Future<StakingWalletBalance> getStakingWalletBalance(String coinUnit) async {
  143. final resp = await _dio.post<Map<String, dynamic>>(
  144. 'uc/staking/wallet',
  145. data: {'coinUnit': coinUnit},
  146. options: Options(contentType: Headers.formUrlEncodedContentType),
  147. );
  148. final raw = resp.data?['data'];
  149. if (raw is Map<String, dynamic>) {
  150. return StakingWalletBalance.fromJson(raw);
  151. }
  152. return StakingWalletBalance.empty(coinUnit);
  153. }
  154. /// 可提现账户指定币种可用余额(对齐 Web `loadFundCoinBalance`)
  155. ///
  156. /// POST `/uc/asset/wallet`,解析逻辑同 `parseFundWalletBalances`。
  157. Future<String> getFundingWalletAvailable(String coinUnit) async {
  158. try {
  159. final resp = await _dio.post<Map<String, dynamic>>(
  160. 'uc/asset/wallet',
  161. data: <String, dynamic>{},
  162. );
  163. final map = parseFundWalletBalances(resp.data);
  164. return map[coinUnit.toUpperCase()] ?? '0';
  165. } catch (_) {
  166. return '0';
  167. }
  168. }
  169. Future<AirdropEligibility> getAirdropEligibility() async {
  170. final resp =
  171. await _dio.post<Map<String, dynamic>>('uc/airdrop/eligibility');
  172. final raw = resp.data?['data'];
  173. _log(
  174. 'eligibility response dataType=${raw.runtimeType} keys=${raw is Map<String, dynamic> ? raw.keys.toList() : 'n/a'}',
  175. );
  176. if (raw is Map<String, dynamic>) {
  177. return AirdropEligibility.fromJson(raw);
  178. }
  179. _log('eligibility fallback to empty because data is not map');
  180. return AirdropEligibility.empty();
  181. }
  182. Future<void> claimAirdrop() async {
  183. await _dio.post<Map<String, dynamic>>('uc/airdrop/claim');
  184. }
  185. Future<AirdropRecordPage> getAirdropRecords({
  186. required int pageNo,
  187. int pageSize = 10,
  188. }) async {
  189. final resp = await _dio.post<Map<String, dynamic>>(
  190. 'uc/airdrop/records',
  191. data: {'pageNo': pageNo, 'pageSize': pageSize},
  192. options: Options(contentType: Headers.formUrlEncodedContentType),
  193. );
  194. final raw = resp.data?['data'];
  195. _log(
  196. 'records response pageNo=$pageNo pageSize=$pageSize dataType=${raw.runtimeType}');
  197. if (raw is! Map<String, dynamic>) {
  198. _log('records fallback empty: data is not map');
  199. return const AirdropRecordPage(content: [], totalPages: 0, pageNo: 1);
  200. }
  201. final contentRaw = raw['content'];
  202. final content = contentRaw is List
  203. ? contentRaw
  204. .whereType<Map<String, dynamic>>()
  205. .map(AirdropRecordItem.fromJson)
  206. .toList()
  207. : <AirdropRecordItem>[];
  208. final totalPages = int.tryParse('${raw['totalPages'] ?? 0}') ?? 0;
  209. // 后端常见两种分页字段:
  210. // 1) pageNo: 1 基页码
  211. // 2) number: 0 基页码(Spring Pageable)
  212. final rawPageNo = int.tryParse('${raw['pageNo'] ?? ''}');
  213. final rawNumber = int.tryParse('${raw['number'] ?? ''}');
  214. final parsedPageNo = rawPageNo ?? ((rawNumber ?? (pageNo - 1)) + 1);
  215. _log(
  216. 'records parsed pageNo=$parsedPageNo(totalPages=$totalPages, rawPageNo=$rawPageNo, rawNumber=$rawNumber) contentRawType=${contentRaw.runtimeType} contentCount=${content.length}',
  217. );
  218. return AirdropRecordPage(
  219. content: content,
  220. totalPages: totalPages,
  221. pageNo: parsedPageNo,
  222. );
  223. }
  224. }