| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- import 'dart:async';
- import 'package:decimal/decimal.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../core/network/dio_client.dart';
- import '../core/utils/dialog_utils.dart' show extractErrorMessage;
- import '../data/models/asset/account_auth.dart';
- import '../data/models/asset/withdraw_balance.dart';
- import '../data/models/asset/withdraw_coin_option.dart';
- import '../data/models/asset/withdraw_record.dart';
- import '../data/services/withdraw_service.dart';
- // ══════════════════════════════════════════════════════════════
- // Withdraw State
- // ══════════════════════════════════════════════════════════════
- class WithdrawState {
- /// 0=链上提币, 1=内部转账
- final int tabIndex;
- final List<WithdrawNetworkItem> networkList;
- final int selectedNetworkId;
- final List<WithdrawCoinOption> onchainSubCoins;
- final bool onchainSubCoinsLoading;
- final int selectedSubCoinIndex;
- final List<WithdrawCoinOption> transferMainCoins;
- final int selectedTransferCoinIndex;
- final List<SupportCoinInfo> coinInfoList;
- final WithdrawBalance? balance;
- final AccountAuth? auth;
- final int codeCountdown;
- final bool isSubmitting;
- final bool isLoading;
- final String? errorMessage;
- const WithdrawState({
- this.tabIndex = 0,
- this.networkList = const [],
- this.selectedNetworkId = 0,
- this.onchainSubCoins = const [],
- this.onchainSubCoinsLoading = false,
- this.selectedSubCoinIndex = 0,
- this.transferMainCoins = const [],
- this.selectedTransferCoinIndex = 0,
- this.coinInfoList = const [],
- this.balance,
- this.auth,
- this.codeCountdown = 0,
- this.isSubmitting = false,
- this.isLoading = false,
- this.errorMessage,
- });
- WithdrawCoinOption? get onchainSelectedCoin =>
- onchainSubCoins.isNotEmpty && selectedSubCoinIndex < onchainSubCoins.length
- ? onchainSubCoins[selectedSubCoinIndex]
- : null;
- WithdrawCoinOption? get transferSelectedCoin =>
- transferMainCoins.isNotEmpty &&
- selectedTransferCoinIndex < transferMainCoins.length
- ? transferMainCoins[selectedTransferCoinIndex]
- : null;
- Decimal get fee {
- final coin = onchainSelectedCoin;
- if (coin == null) {
- return Decimal.zero;
- }
- return Decimal.tryParse(coin.withdrawFee) ?? Decimal.zero;
- }
- Decimal get minWithdrawAmount {
- final coin = onchainSelectedCoin;
- if (coin == null) {
- return Decimal.zero;
- }
- final min = Decimal.tryParse(coin.minWithdraw) ?? Decimal.zero;
- if (min > Decimal.zero) {
- return min;
- }
- final defaults = networkWithdrawDefaults[coin.networkLabel];
- if (defaults != null) {
- return Decimal.tryParse(defaults.min) ?? Decimal.zero;
- }
- return Decimal.zero;
- }
- Decimal get transferMinAmount {
- final coin = transferSelectedCoin;
- if (coin == null) {
- return Decimal.zero;
- }
- return Decimal.tryParse(coin.minTransfer) ?? Decimal.zero;
- }
- Decimal get currentMinAmount =>
- tabIndex == 0 ? minWithdrawAmount : transferMinAmount;
- Decimal get withdrawableBalance =>
- _balanceForCoin(onchainSelectedCoin, isTransfer: false);
- Decimal get transferableBalance =>
- _balanceForCoin(transferSelectedCoin, isTransfer: true);
- Decimal get availableBalance =>
- tabIndex == 0 ? withdrawableBalance : transferableBalance;
- String get onchainCoinSymbol =>
- symbolForWithdrawCoin(onchainSelectedCoin);
- String get transferCoinSymbol =>
- symbolForWithdrawCoin(transferSelectedCoin);
- String get onChainSubmitUnit =>
- onchainSelectedCoin?.unit.toLowerCase() ?? 'usdt';
- String get transferSubmitUnit =>
- transferSelectedCoin?.unit.toLowerCase() ?? 'usdt';
- bool get isGoogleBound => auth?.isGoogleVerified ?? false;
- Decimal _balanceForCoin(WithdrawCoinOption? coin, {required bool isTransfer}) {
- if (coin == null) {
- return Decimal.zero;
- }
- if (isTransfer && coin.unit.toUpperCase() == 'USDT') {
- return balance?.transferableBalance ?? balance?.balance ?? Decimal.zero;
- }
- if (!isTransfer && coin.isUsdtFamily) {
- return balance?.withdrawableBalance ?? balance?.balance ?? Decimal.zero;
- }
- return Decimal.tryParse(coin.balance) ?? Decimal.zero;
- }
- WithdrawState copyWith({
- int? tabIndex,
- List<WithdrawNetworkItem>? networkList,
- int? selectedNetworkId,
- List<WithdrawCoinOption>? onchainSubCoins,
- bool? onchainSubCoinsLoading,
- int? selectedSubCoinIndex,
- List<WithdrawCoinOption>? transferMainCoins,
- int? selectedTransferCoinIndex,
- List<SupportCoinInfo>? coinInfoList,
- WithdrawBalance? balance,
- AccountAuth? auth,
- int? codeCountdown,
- bool? isSubmitting,
- bool? isLoading,
- String? errorMessage,
- }) =>
- WithdrawState(
- tabIndex: tabIndex ?? this.tabIndex,
- networkList: networkList ?? this.networkList,
- selectedNetworkId: selectedNetworkId ?? this.selectedNetworkId,
- onchainSubCoins: onchainSubCoins ?? this.onchainSubCoins,
- onchainSubCoinsLoading:
- onchainSubCoinsLoading ?? this.onchainSubCoinsLoading,
- selectedSubCoinIndex: selectedSubCoinIndex ?? this.selectedSubCoinIndex,
- transferMainCoins: transferMainCoins ?? this.transferMainCoins,
- selectedTransferCoinIndex:
- selectedTransferCoinIndex ?? this.selectedTransferCoinIndex,
- coinInfoList: coinInfoList ?? this.coinInfoList,
- balance: balance ?? this.balance,
- auth: auth ?? this.auth,
- codeCountdown: codeCountdown ?? this.codeCountdown,
- isSubmitting: isSubmitting ?? this.isSubmitting,
- isLoading: isLoading ?? this.isLoading,
- errorMessage: errorMessage,
- );
- }
- // ══════════════════════════════════════════════════════════════
- // Withdraw Notifier
- // ══════════════════════════════════════════════════════════════
- class WithdrawNotifier extends Notifier<WithdrawState> {
- Timer? _countdownTimer;
- @override
- WithdrawState build() {
- ref.onDispose(() => _countdownTimer?.cancel());
- Future.microtask(_load);
- return const WithdrawState(isLoading: true);
- }
- Future<void> _load() async {
- state = state.copyWith(isLoading: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- final withdrawService = WithdrawService(dio);
- final results = await Future.wait([
- withdrawService.getBalance('usdt'),
- withdrawService.getSecuritySetting(),
- withdrawService.getSupportCoinInfo(),
- withdrawService.getWithdrawNetworks(),
- ]);
- final balance = results[0] as WithdrawBalance;
- final auth = results[1] as AccountAuth;
- final coinInfoList = results[2] as List<SupportCoinInfo>;
- final networkList = results[3] as List<WithdrawNetworkItem>;
- final transferMainCoins =
- await withdrawService.getTransferParents(coinInfoList: coinInfoList);
- state = state.copyWith(
- balance: balance,
- auth: auth,
- coinInfoList: coinInfoList,
- networkList: networkList,
- transferMainCoins: transferMainCoins,
- isLoading: false,
- );
- await _resetSelection();
- } catch (e) {
- state = state.copyWith(
- isLoading: false,
- errorMessage: extractErrorMessage(e),
- );
- }
- }
- Future<void> _resetSelection() async {
- final nets = state.networkList;
- var networkId = state.selectedNetworkId;
- if (nets.isEmpty) {
- networkId = 0;
- state = state.copyWith(
- selectedNetworkId: 0,
- onchainSubCoins: [],
- selectedSubCoinIndex: 0,
- );
- } else {
- if (!nets.any((n) => n.id == networkId)) {
- networkId = nets.first.id;
- }
- state = state.copyWith(
- selectedNetworkId: networkId,
- selectedSubCoinIndex: 0,
- );
- await _loadOnchainSubCoins(networkId);
- }
- var transferIdx = state.selectedTransferCoinIndex;
- if (transferIdx >= state.transferMainCoins.length) {
- transferIdx = 0;
- state = state.copyWith(selectedTransferCoinIndex: transferIdx);
- }
- }
- Future<void> _loadOnchainSubCoins(int networkId) async {
- if (networkId <= 0) {
- state = state.copyWith(onchainSubCoins: []);
- return;
- }
- state = state.copyWith(onchainSubCoinsLoading: true);
- try {
- final dio = ref.read(dioClientProvider);
- final coins = await WithdrawService(dio).getWithdrawConfigs(
- networkId: networkId,
- coinInfoList: state.coinInfoList,
- );
- var subIdx = state.selectedSubCoinIndex;
- if (subIdx >= coins.length) {
- subIdx = 0;
- }
- state = state.copyWith(
- onchainSubCoins: coins,
- selectedSubCoinIndex: subIdx,
- onchainSubCoinsLoading: false,
- );
- } catch (e) {
- state = state.copyWith(
- onchainSubCoins: [],
- onchainSubCoinsLoading: false,
- errorMessage: extractErrorMessage(e),
- );
- }
- }
- Future<void> refresh() => _load();
- void setTab(int index) {
- state = state.copyWith(tabIndex: index);
- }
- Future<void> selectNetwork(int networkId) async {
- if (networkId <= 0 ||
- !state.networkList.any((n) => n.id == networkId)) {
- return;
- }
- state = state.copyWith(
- selectedNetworkId: networkId,
- selectedSubCoinIndex: 0,
- );
- await _loadOnchainSubCoins(networkId);
- }
- void selectSubCoin(int index) {
- if (index >= 0 && index < state.onchainSubCoins.length) {
- state = state.copyWith(selectedSubCoinIndex: index);
- }
- }
- void selectTransferCoin(int index) {
- if (index >= 0 && index < state.transferMainCoins.length) {
- state = state.copyWith(selectedTransferCoinIndex: index);
- }
- }
- Future<String?> sendEmailCode({
- required String address,
- required String amount,
- }) async {
- if (state.tabIndex == 0 && state.onchainSelectedCoin == null) {
- return 'errSelectNetwork';
- }
- if (state.tabIndex == 1 && state.transferSelectedCoin == null) {
- return 'errSelectCoin';
- }
- if (address.isEmpty) {
- return 'errEnterAddress';
- }
- if (amount.isEmpty) {
- return 'errEnterAmount';
- }
- try {
- final dio = ref.read(dioClientProvider);
- final unit = state.tabIndex == 1
- ? state.transferSubmitUnit
- : state.onChainSubmitUnit;
- await WithdrawService(dio).sendWithdrawEmailCode(
- unit: unit,
- address: address,
- amount: amount,
- );
- state = state.copyWith(codeCountdown: 60);
- _countdownTimer?.cancel();
- _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
- final remaining = state.codeCountdown - 1;
- if (remaining <= 0) {
- timer.cancel();
- state = state.copyWith(codeCountdown: 0);
- } else {
- state = state.copyWith(codeCountdown: remaining);
- }
- });
- return null;
- } catch (e) {
- return extractErrorMessage(e);
- }
- }
- Future<bool> submitOnChainWithdraw({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) async {
- state = state.copyWith(isSubmitting: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).withdrawApply(
- unit: state.onChainSubmitUnit,
- amount: amount,
- address: address,
- fee: state.fee.toString(),
- vcode: vcode,
- jyPassword: jyPassword,
- vcode2: vcode2,
- );
- state = state.copyWith(isSubmitting: false);
- await _load();
- return true;
- } catch (e) {
- state = state.copyWith(
- isSubmitting: false,
- errorMessage: extractErrorMessage(e),
- );
- return false;
- }
- }
- Future<bool> submitInternalTransfer({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) async {
- state = state.copyWith(isSubmitting: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).internalTransfer(
- unit: state.transferSubmitUnit,
- amount: amount,
- address: address,
- vcode: vcode,
- jyPassword: jyPassword,
- vcode2: vcode2,
- );
- state = state.copyWith(isSubmitting: false);
- await _load();
- return true;
- } catch (e) {
- state = state.copyWith(
- isSubmitting: false,
- errorMessage: extractErrorMessage(e),
- );
- return false;
- }
- }
- String? validate({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) {
- if (state.tabIndex == 0 && state.onchainSelectedCoin == null) {
- return 'errSelectNetwork';
- }
- if (state.tabIndex == 1 && state.transferSelectedCoin == null) {
- return 'errSelectCoin';
- }
- if (address.isEmpty) {
- return 'errEnterAddress';
- }
- if (amount.isEmpty) {
- return 'errEnterAmount';
- }
- if (jyPassword.isEmpty) {
- return 'errEnterFundPassword';
- }
- if (vcode.isEmpty) {
- return 'errEnterVerifyCode';
- }
- if (!state.isGoogleBound) {
- return 'errBindGoogleFirst';
- }
- if (vcode2.isEmpty) {
- return 'errEnterGoogleCode';
- }
- final amountDecimal = Decimal.tryParse(amount);
- if (amountDecimal == null) {
- return 'errAmountFormat';
- }
- if (amountDecimal < state.currentMinAmount) {
- final coinSymbol = state.tabIndex == 0
- ? state.onchainCoinSymbol
- : state.transferCoinSymbol;
- return state.tabIndex == 0
- ? 'errMinWithdraw:${state.currentMinAmount}:$coinSymbol'
- : 'errMinTransfer:${state.currentMinAmount}:$coinSymbol';
- }
- if (amountDecimal > state.availableBalance) {
- return 'errExceedBalance';
- }
- return null;
- }
- }
- final withdrawProvider = NotifierProvider<WithdrawNotifier, WithdrawState>(
- WithdrawNotifier.new,
- );
- // ══════════════════════════════════════════════════════════════
- // Withdraw History
- // ══════════════════════════════════════════════════════════════
- class WithdrawHistoryState {
- final List<WithdrawRecord> records;
- final bool isLoading;
- final bool hasMore;
- /// 链上提币当前页(后端 page 从 0 开始)
- final int withdrawPage;
- /// 内部转账当前页(后端 pageNo 从 0 开始)
- final int transferPage;
- final String? errorMessage;
- const WithdrawHistoryState({
- this.records = const [],
- this.isLoading = false,
- this.hasMore = true,
- this.withdrawPage = 0,
- this.transferPage = 0,
- this.errorMessage,
- });
- WithdrawHistoryState copyWith({
- List<WithdrawRecord>? records,
- bool? isLoading,
- bool? hasMore,
- int? withdrawPage,
- int? transferPage,
- String? errorMessage,
- }) =>
- WithdrawHistoryState(
- records: records ?? this.records,
- isLoading: isLoading ?? this.isLoading,
- hasMore: hasMore ?? this.hasMore,
- withdrawPage: withdrawPage ?? this.withdrawPage,
- transferPage: transferPage ?? this.transferPage,
- errorMessage: errorMessage,
- );
- }
- class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
- static const _pageSize = 10;
- @override
- WithdrawHistoryState build() {
- Future.microtask(_loadFirst);
- return const WithdrawHistoryState(isLoading: true);
- }
- Future<void> _loadFirst() async {
- state = state.copyWith(isLoading: true, errorMessage: null);
- try {
- final service = WithdrawService(ref.read(dioClientProvider));
- final results = await Future.wait([
- service.getWithdrawRecords(page: 0, pageSize: _pageSize),
- service.getTransferRecords(pageNo: 0, pageSize: _pageSize),
- ]);
- final withdrawRecords = results[0] as List<WithdrawRecord>;
- final transferRecords = (results[1] as List<WithdrawRecord>)
- .map((r) => r.copyWith(isTransfer: true))
- .toList();
- final all = _merge(withdrawRecords, transferRecords);
- state = state.copyWith(
- records: all,
- isLoading: false,
- withdrawPage: 0,
- transferPage: 0,
- hasMore: withdrawRecords.length >= _pageSize ||
- transferRecords.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
- }
- }
- Future<void> refresh() => _loadFirst();
- Future<void> loadMore() async {
- if (state.isLoading || !state.hasMore) {
- return;
- }
- state = state.copyWith(isLoading: true);
- try {
- final service = WithdrawService(ref.read(dioClientProvider));
- final nextW = state.withdrawPage + 1;
- final nextT = state.transferPage + 1;
- final results = await Future.wait([
- service.getWithdrawRecords(page: nextW, pageSize: _pageSize),
- service.getTransferRecords(pageNo: nextT, pageSize: _pageSize),
- ]);
- final withdrawRecords = results[0] as List<WithdrawRecord>;
- final transferRecords = (results[1] as List<WithdrawRecord>)
- .map((r) => r.copyWith(isTransfer: true))
- .toList();
- final appended = _merge(withdrawRecords, transferRecords);
- state = state.copyWith(
- records: [...state.records, ...appended],
- isLoading: false,
- withdrawPage: nextW,
- transferPage: nextT,
- hasMore: withdrawRecords.length >= _pageSize ||
- transferRecords.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
- }
- }
- List<WithdrawRecord> _merge(
- List<WithdrawRecord> withdrawRecords,
- List<WithdrawRecord> transferRecords,
- ) {
- final all = [...withdrawRecords, ...transferRecords];
- all.sort((a, b) => b.createTime.compareTo(a.createTime));
- return all;
- }
- Future<bool> cancelWithdraw(String id) async {
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).cancelWithdraw(id);
- await refresh();
- return true;
- } catch (e) {
- state = state.copyWith(errorMessage: extractErrorMessage(e));
- return false;
- }
- }
- }
- final withdrawHistoryProvider =
- NotifierProvider<WithdrawHistoryNotifier, WithdrawHistoryState>(
- WithdrawHistoryNotifier.new,
- );
|