withdraw_provider.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import 'dart:async';
  2. import 'package:decimal/decimal.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../core/network/dio_client.dart';
  5. import '../core/utils/dialog_utils.dart' show extractErrorMessage;
  6. import '../data/models/asset/account_auth.dart';
  7. import '../data/models/asset/deposit_wallet.dart';
  8. import '../data/models/asset/withdraw_balance.dart';
  9. import '../data/models/asset/withdraw_record.dart';
  10. import '../data/services/asset_service.dart';
  11. import '../data/services/withdraw_service.dart';
  12. // ══════════════════════════════════════════════════════════════
  13. // Withdraw State
  14. // ══════════════════════════════════════════════════════════════
  15. class WithdrawState {
  16. /// 0=链上提币, 1=内部转账
  17. final int tabIndex;
  18. /// 钱包列表(含网络信息,复用充币的 DepositWallet)
  19. final List<DepositWallet> usdtWallets;
  20. /// 当前选中的网络索引
  21. final int selectedNetworkIndex;
  22. /// 可用余额
  23. final WithdrawBalance? balance;
  24. /// 认证信息
  25. final AccountAuth? auth;
  26. /// 验证码倒计时秒数(0=可发送)
  27. final int codeCountdown;
  28. /// 提交中
  29. final bool isSubmitting;
  30. final bool isLoading;
  31. final String? errorMessage;
  32. /// 内部转账最小金额(USDT 基础配置,null 表示未加载)
  33. final Decimal? _transferMinAmount;
  34. const WithdrawState({
  35. this.tabIndex = 0,
  36. this.usdtWallets = const [],
  37. this.selectedNetworkIndex = -1,
  38. this.balance,
  39. this.auth,
  40. Decimal? transferMinAmount,
  41. this.codeCountdown = 0,
  42. this.isSubmitting = false,
  43. this.isLoading = false,
  44. this.errorMessage,
  45. }) : _transferMinAmount = transferMinAmount;
  46. /// 当前选中的钱包(链上提币模式下使用)
  47. DepositWallet? get selectedWallet =>
  48. selectedNetworkIndex >= 0 && usdtWallets.isNotEmpty && selectedNetworkIndex < usdtWallets.length
  49. ? usdtWallets[selectedNetworkIndex]
  50. : null;
  51. /// 当前币种信息
  52. WalletCoin? get currentCoin => selectedWallet?.coin;
  53. /// 网络名称列表
  54. List<String> get networkNames =>
  55. usdtWallets.map((w) => w.coin?.networkName ?? '').toList();
  56. /// 链上提币的 unit(币种代号)
  57. String get onChainUnit => currentCoin?.code ?? 'TUSDT';
  58. /// 手续费(withdrawFeeValue)
  59. Decimal get fee => currentCoin?.fee ?? Decimal.zero;
  60. /// 链上提币最小额
  61. Decimal get minWithdrawAmount => currentCoin?.minWithdrawAmount ?? Decimal.zero;
  62. /// 内部转账最小额(USDT 基础配置)
  63. Decimal get transferMinAmount => _transferMinAmount ?? Decimal.zero;
  64. /// 当前模式下的最小额(链上 or 内部转账)
  65. Decimal get currentMinAmount => tabIndex == 0 ? minWithdrawAmount : transferMinAmount;
  66. /// 可提现余额(链上提币)
  67. Decimal get withdrawableBalance => balance?.withdrawableBalance ?? Decimal.zero;
  68. /// 可转账余额(内部转账)
  69. Decimal get transferableBalance => balance?.transferableBalance ?? Decimal.zero;
  70. /// 当前可用余额(根据 Tab 决定)
  71. Decimal get availableBalance =>
  72. tabIndex == 0 ? withdrawableBalance : transferableBalance;
  73. /// 是否已绑定 Google 验证
  74. bool get isGoogleBound => auth?.isGoogleVerified ?? false;
  75. WithdrawState copyWith({
  76. int? tabIndex,
  77. List<DepositWallet>? usdtWallets,
  78. int? selectedNetworkIndex,
  79. WithdrawBalance? balance,
  80. AccountAuth? auth,
  81. Decimal? transferMinAmount,
  82. int? codeCountdown,
  83. bool? isSubmitting,
  84. bool? isLoading,
  85. String? errorMessage,
  86. }) =>
  87. WithdrawState(
  88. tabIndex: tabIndex ?? this.tabIndex,
  89. usdtWallets: usdtWallets ?? this.usdtWallets,
  90. selectedNetworkIndex: selectedNetworkIndex ?? this.selectedNetworkIndex,
  91. balance: balance ?? this.balance,
  92. auth: auth ?? this.auth,
  93. transferMinAmount: transferMinAmount ?? _transferMinAmount,
  94. codeCountdown: codeCountdown ?? this.codeCountdown,
  95. isSubmitting: isSubmitting ?? this.isSubmitting,
  96. isLoading: isLoading ?? this.isLoading,
  97. errorMessage: errorMessage,
  98. );
  99. }
  100. // ══════════════════════════════════════════════════════════════
  101. // Withdraw Notifier
  102. // ══════════════════════════════════════════════════════════════
  103. class WithdrawNotifier extends Notifier<WithdrawState> {
  104. Timer? _countdownTimer;
  105. @override
  106. WithdrawState build() {
  107. ref.onDispose(() => _countdownTimer?.cancel());
  108. Future.microtask(_load);
  109. return const WithdrawState(isLoading: true);
  110. }
  111. Future<void> _load() async {
  112. state = state.copyWith(isLoading: true, errorMessage: null, selectedNetworkIndex: -1);
  113. try {
  114. final dio = ref.read(dioClientProvider);
  115. final assetService = AssetService(dio);
  116. final withdrawService = WithdrawService(dio);
  117. // 并发请求:钱包地址 + 可用余额 + 认证信息 + 内部转账最小额
  118. final results = await Future.wait([
  119. assetService.getWalletAddresses(),
  120. withdrawService.getBalance('usdt'),
  121. withdrawService.getSecuritySetting(),
  122. withdrawService.getTransferMinAmount(),
  123. ]);
  124. final wallets = results[0] as List<DepositWallet>;
  125. final balance = results[1] as WithdrawBalance;
  126. final auth = results[2] as AccountAuth;
  127. final transferMinAmount = results[3] as Decimal;
  128. // 过滤 USDT 钱包,TRC20 排最前
  129. const networkOrder = {'TRC20': 0, 'ERC20': 1, 'BEP20': 2};
  130. final usdtWallets = wallets
  131. .where((w) => w.coin != null && w.coin!.code.contains('USDT'))
  132. .toList()
  133. ..sort((a, b) {
  134. final oa = networkOrder[a.coin!.networkName] ?? 99;
  135. final ob = networkOrder[b.coin!.networkName] ?? 99;
  136. return oa.compareTo(ob);
  137. });
  138. state = state.copyWith(
  139. usdtWallets: usdtWallets,
  140. balance: balance,
  141. auth: auth,
  142. transferMinAmount: transferMinAmount,
  143. selectedNetworkIndex: -1,
  144. isLoading: false,
  145. );
  146. } catch (e) {
  147. state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
  148. }
  149. }
  150. Future<void> refresh() => _load();
  151. void setTab(int index) {
  152. state = state.copyWith(tabIndex: index);
  153. }
  154. void selectNetwork(int index) {
  155. state = state.copyWith(selectedNetworkIndex: index);
  156. }
  157. /// 发送邮箱验证码,返回错误信息(null 表示成功)
  158. Future<String?> sendEmailCode({
  159. required String address,
  160. required String amount,
  161. }) async {
  162. if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
  163. if (address.isEmpty) return 'errEnterAddress';
  164. if (amount.isEmpty) return 'errEnterAmount';
  165. try {
  166. final dio = ref.read(dioClientProvider);
  167. await WithdrawService(dio).sendWithdrawEmailCode(
  168. unit: state.tabIndex == 1 ? 'USDT' : state.onChainUnit,
  169. address: address,
  170. amount: amount,
  171. );
  172. // 启动 60 秒倒计时
  173. state = state.copyWith(codeCountdown: 60);
  174. _countdownTimer?.cancel();
  175. _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  176. final remaining = state.codeCountdown - 1;
  177. if (remaining <= 0) {
  178. timer.cancel();
  179. state = state.copyWith(codeCountdown: 0);
  180. } else {
  181. state = state.copyWith(codeCountdown: remaining);
  182. }
  183. });
  184. return null;
  185. } catch (e) {
  186. return extractErrorMessage(e);
  187. }
  188. }
  189. /// 链上提币提交
  190. Future<bool> submitOnChainWithdraw({
  191. required String address,
  192. required String amount,
  193. required String jyPassword,
  194. required String vcode,
  195. required String vcode2,
  196. }) async {
  197. state = state.copyWith(isSubmitting: true, errorMessage: null);
  198. try {
  199. final dio = ref.read(dioClientProvider);
  200. await WithdrawService(dio).withdrawApply(
  201. unit: state.onChainUnit,
  202. amount: amount,
  203. address: address,
  204. fee: state.fee.toString(),
  205. vcode: vcode,
  206. jyPassword: jyPassword,
  207. vcode2: vcode2,
  208. );
  209. state = state.copyWith(isSubmitting: false);
  210. // 刷新余额
  211. _load();
  212. return true;
  213. } catch (e) {
  214. state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
  215. return false;
  216. }
  217. }
  218. /// 内部转账提交
  219. Future<bool> submitInternalTransfer({
  220. required String address,
  221. required String amount,
  222. required String jyPassword,
  223. required String vcode,
  224. required String vcode2,
  225. }) async {
  226. state = state.copyWith(isSubmitting: true, errorMessage: null);
  227. try {
  228. final dio = ref.read(dioClientProvider);
  229. await WithdrawService(dio).internalTransfer(
  230. unit: 'USDT', // 内部转账用币种名
  231. amount: amount,
  232. address: address,
  233. vcode: vcode,
  234. jyPassword: jyPassword,
  235. vcode2: vcode2,
  236. );
  237. state = state.copyWith(isSubmitting: false);
  238. _load();
  239. return true;
  240. } catch (e) {
  241. state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
  242. return false;
  243. }
  244. }
  245. /// 表单校验
  246. String? validate({
  247. required String address,
  248. required String amount,
  249. required String jyPassword,
  250. required String vcode,
  251. required String vcode2,
  252. }) {
  253. if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
  254. if (address.isEmpty) return 'errEnterAddress';
  255. if (amount.isEmpty) return 'errEnterAmount';
  256. if (jyPassword.isEmpty) return 'errEnterFundPassword';
  257. if (vcode.isEmpty) return 'errEnterVerifyCode';
  258. if (!state.isGoogleBound) return 'errBindGoogleFirst';
  259. if (vcode2.isEmpty) return 'errEnterGoogleCode';
  260. final amountDecimal = Decimal.tryParse(amount);
  261. if (amountDecimal == null) return 'errAmountFormat';
  262. if (amountDecimal < state.currentMinAmount) {
  263. return state.tabIndex == 0
  264. ? 'errMinWithdraw:${state.currentMinAmount}'
  265. : 'errMinTransfer:${state.currentMinAmount}';
  266. }
  267. if (amountDecimal > state.availableBalance) {
  268. return 'errExceedBalance';
  269. }
  270. return null;
  271. }
  272. }
  273. final withdrawProvider = NotifierProvider<WithdrawNotifier, WithdrawState>(
  274. WithdrawNotifier.new,
  275. );
  276. // ══════════════════════════════════════════════════════════════
  277. // Withdraw History
  278. // ══════════════════════════════════════════════════════════════
  279. class WithdrawHistoryState {
  280. final List<WithdrawRecord> records;
  281. final bool isLoading;
  282. final bool hasMore;
  283. /// 链上提币当前页(后端 page 从 0 开始)
  284. final int withdrawPage;
  285. /// 内部转账当前页(后端 pageNo 从 0 开始)
  286. final int transferPage;
  287. final String? errorMessage;
  288. const WithdrawHistoryState({
  289. this.records = const [],
  290. this.isLoading = false,
  291. this.hasMore = true,
  292. this.withdrawPage = 0,
  293. this.transferPage = 0,
  294. this.errorMessage,
  295. });
  296. WithdrawHistoryState copyWith({
  297. List<WithdrawRecord>? records,
  298. bool? isLoading,
  299. bool? hasMore,
  300. int? withdrawPage,
  301. int? transferPage,
  302. String? errorMessage,
  303. }) =>
  304. WithdrawHistoryState(
  305. records: records ?? this.records,
  306. isLoading: isLoading ?? this.isLoading,
  307. hasMore: hasMore ?? this.hasMore,
  308. withdrawPage: withdrawPage ?? this.withdrawPage,
  309. transferPage: transferPage ?? this.transferPage,
  310. errorMessage: errorMessage,
  311. );
  312. }
  313. class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
  314. static const _pageSize = 10;
  315. @override
  316. WithdrawHistoryState build() {
  317. Future.microtask(_loadFirst);
  318. return const WithdrawHistoryState(isLoading: true);
  319. }
  320. Future<void> _loadFirst() async {
  321. state = state.copyWith(isLoading: true, errorMessage: null);
  322. try {
  323. final service = WithdrawService(ref.read(dioClientProvider));
  324. final results = await Future.wait([
  325. service.getWithdrawRecords(page: 0, pageSize: _pageSize),
  326. service.getTransferRecords(pageNo: 0, pageSize: _pageSize),
  327. ]);
  328. final withdrawRecords = results[0] as List<WithdrawRecord>;
  329. final transferRecords = (results[1] as List<WithdrawRecord>)
  330. .map((r) => r.copyWith(isTransfer: true))
  331. .toList();
  332. final all = _merge(withdrawRecords, transferRecords);
  333. state = state.copyWith(
  334. records: all,
  335. isLoading: false,
  336. withdrawPage: 0,
  337. transferPage: 0,
  338. hasMore: withdrawRecords.length >= _pageSize ||
  339. transferRecords.length >= _pageSize,
  340. );
  341. } catch (e) {
  342. state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
  343. }
  344. }
  345. Future<void> refresh() => _loadFirst();
  346. Future<void> loadMore() async {
  347. if (state.isLoading || !state.hasMore) return;
  348. state = state.copyWith(isLoading: true);
  349. try {
  350. final service = WithdrawService(ref.read(dioClientProvider));
  351. final nextW = state.withdrawPage + 1;
  352. final nextT = state.transferPage + 1;
  353. final results = await Future.wait([
  354. service.getWithdrawRecords(page: nextW, pageSize: _pageSize),
  355. service.getTransferRecords(pageNo: nextT, pageSize: _pageSize),
  356. ]);
  357. final withdrawRecords = results[0] as List<WithdrawRecord>;
  358. final transferRecords = (results[1] as List<WithdrawRecord>)
  359. .map((r) => r.copyWith(isTransfer: true))
  360. .toList();
  361. final appended = _merge(withdrawRecords, transferRecords);
  362. state = state.copyWith(
  363. records: [...state.records, ...appended],
  364. isLoading: false,
  365. withdrawPage: nextW,
  366. transferPage: nextT,
  367. hasMore: withdrawRecords.length >= _pageSize ||
  368. transferRecords.length >= _pageSize,
  369. );
  370. } catch (e) {
  371. state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
  372. }
  373. }
  374. /// 合并两类记录,按 createTime 倒序
  375. List<WithdrawRecord> _merge(
  376. List<WithdrawRecord> withdrawRecords,
  377. List<WithdrawRecord> transferRecords,
  378. ) {
  379. final all = [...withdrawRecords, ...transferRecords];
  380. all.sort((a, b) => b.createTime.compareTo(a.createTime));
  381. return all;
  382. }
  383. /// 取消链上提现
  384. Future<bool> cancelWithdraw(String id) async {
  385. try {
  386. final dio = ref.read(dioClientProvider);
  387. await WithdrawService(dio).cancelWithdraw(id);
  388. await refresh();
  389. return true;
  390. } catch (e) {
  391. state = state.copyWith(errorMessage: extractErrorMessage(e));
  392. return false;
  393. }
  394. }
  395. }
  396. final withdrawHistoryProvider =
  397. NotifierProvider<WithdrawHistoryNotifier, WithdrawHistoryState>(
  398. WithdrawHistoryNotifier.new,
  399. );