| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- import 'package:decimal/decimal.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../core/network/dio_client.dart';
- import '../core/utils/dialog_utils.dart';
- import '../core/utils/spot_transfer_asset.dart';
- import '../core/utils/transfer_pair.dart';
- import '../data/models/asset/transfer_record.dart';
- import '../data/models/asset/today_pnl.dart';
- import '../data/services/asset_service.dart';
- import '../data/services/spot_service.dart';
- class TransferInitOptions {
- const TransferInitOptions({
- this.from,
- this.to,
- this.defaultSymbol = 'USDT',
- this.preferDefaultSymbol = false,
- this.spotTradingBridgeOnly = false,
- });
- final String? from;
- final String? to;
- final String defaultSymbol;
- final bool preferDefaultSymbol;
- final bool spotTradingBridgeOnly;
- }
- class TransferState {
- final String fromType;
- final String toType;
- final bool spotTradingBridgeOnly;
- final bool preferDefaultSymbol;
- final List<String> openCoins;
- final Map<String, String> fundAvailable;
- final Map<String, String> spotAvailable;
- final Map<String, String> legacyBalances;
- final String spotTradingUsdtBalance;
- final String selectedCoin;
- final bool isLoading;
- final bool isSubmitting;
- final bool isLoadingCoins;
- final String? errorMessage;
- const TransferState({
- this.fromType = kWalletSpot,
- this.toType = kWalletSwap,
- this.spotTradingBridgeOnly = false,
- this.preferDefaultSymbol = false,
- this.openCoins = const ['USDT'],
- this.fundAvailable = const {},
- this.spotAvailable = const {},
- this.legacyBalances = const {},
- this.spotTradingUsdtBalance = '0',
- this.selectedCoin = 'USDT',
- this.isLoading = false,
- this.isSubmitting = false,
- this.isLoadingCoins = false,
- this.errorMessage,
- });
- bool get isSpotCoinTransfer =>
- involvesSpotTradingBridge(fromType, toType);
- String get displayCoinUnit => isSpotCoinTransfer ? selectedCoin : 'USDT';
- List<String> get transferOtherTypes {
- if (spotTradingBridgeOnly) {
- return const [kWalletSpotTrading];
- }
- return kOtherWalletTypes;
- }
- Decimal get fromBalance {
- if (isSpotCoinTransfer) {
- if (fromType == kWalletSpotTrading) {
- return _d(spotAvailable[selectedCoin.toUpperCase()] ?? '0');
- }
- if (fromType == kWalletSpot) {
- return _d(fundAvailable[selectedCoin.toUpperCase()] ?? '0');
- }
- }
- if (fromType == kWalletSpotTrading) {
- return _d(spotTradingUsdtBalance);
- }
- return _d(legacyBalances[fromType] ?? '0');
- }
- TransferState copyWith({
- String? fromType,
- String? toType,
- bool? spotTradingBridgeOnly,
- bool? preferDefaultSymbol,
- List<String>? openCoins,
- Map<String, String>? fundAvailable,
- Map<String, String>? spotAvailable,
- Map<String, String>? legacyBalances,
- String? spotTradingUsdtBalance,
- String? selectedCoin,
- bool? isLoading,
- bool? isSubmitting,
- bool? isLoadingCoins,
- String? errorMessage,
- }) {
- return TransferState(
- fromType: fromType ?? this.fromType,
- toType: toType ?? this.toType,
- spotTradingBridgeOnly:
- spotTradingBridgeOnly ?? this.spotTradingBridgeOnly,
- preferDefaultSymbol: preferDefaultSymbol ?? this.preferDefaultSymbol,
- openCoins: openCoins ?? this.openCoins,
- fundAvailable: fundAvailable ?? this.fundAvailable,
- spotAvailable: spotAvailable ?? this.spotAvailable,
- legacyBalances: legacyBalances ?? this.legacyBalances,
- spotTradingUsdtBalance:
- spotTradingUsdtBalance ?? this.spotTradingUsdtBalance,
- selectedCoin: selectedCoin ?? this.selectedCoin,
- isLoading: isLoading ?? this.isLoading,
- isSubmitting: isSubmitting ?? this.isSubmitting,
- isLoadingCoins: isLoadingCoins ?? this.isLoadingCoins,
- errorMessage: errorMessage,
- );
- }
- }
- Decimal _d(String v) => Decimal.tryParse(v) ?? Decimal.zero;
- class TransferNotifier extends AutoDisposeNotifier<TransferState> {
- TransferInitOptions _options = const TransferInitOptions();
- bool _initialized = false;
- @override
- TransferState build() => const TransferState();
- Future<void> init(TransferInitOptions options) async {
- _options = options;
- final pair = options.spotTradingBridgeOnly
- ? normalizeTransferPair(
- options.from ?? kWalletSpot,
- options.to ?? kWalletSpotTrading,
- )
- : normalizeTransferPair(
- options.from ?? kWalletSpot,
- options.to ?? kWalletSwap,
- );
- state = state.copyWith(
- fromType: pair.$1,
- toType: pair.$2,
- spotTradingBridgeOnly: options.spotTradingBridgeOnly,
- preferDefaultSymbol: options.preferDefaultSymbol,
- selectedCoin: options.defaultSymbol.toUpperCase(),
- isLoading: true,
- errorMessage: null,
- );
- await _loadLegacyBalances();
- if (state.isSpotCoinTransfer || options.spotTradingBridgeOnly) {
- await _loadSpotTransferAssets();
- }
- state = state.copyWith(isLoading: false);
- _initialized = true;
- }
- Future<void> _loadLegacyBalances() async {
- try {
- final dio = ref.read(dioClientProvider);
- final assetService = AssetService(dio);
- final spotService = SpotService(dio);
- final results = await Future.wait([
- assetService.getTodayPnl(),
- spotService.getAssets().catchError((_) => <String, dynamic>{}),
- ]);
- final todayPnl = results[0] as TodayPnl;
- final spotData = results[1] as Map<String, dynamic>;
- state = state.copyWith(
- legacyBalances: _legacyBalancesFromTodayPnl(todayPnl),
- spotTradingUsdtBalance:
- parseSpotAvailableMap(spotData)['USDT'] ?? '0',
- );
- } catch (e) {
- state = state.copyWith(errorMessage: e.toString());
- }
- }
- Map<String, String> _legacyBalancesFromTodayPnl(TodayPnl todayPnl) {
- final list = todayPnl.accountInfoList;
- String balAt(int idx) {
- if (idx < 0 || idx >= list.length) {
- return '0';
- }
- return list[idx].balance.toString();
- }
- return {
- kWalletSwap: balAt(kSwapAccountIdx),
- kWalletFollow: balAt(kFollowAccountIdx),
- kWalletSpot: balAt(kSpotAccountIdx),
- };
- }
- Future<void> _loadSpotTransferAssets() async {
- state = state.copyWith(isLoadingCoins: true);
- try {
- final dio = ref.read(dioClientProvider);
- final spotService = SpotService(dio);
- final results = await Future.wait([
- spotService.getCoins().catchError((_) => <Map<String, dynamic>>[]),
- spotService.getAssets().catchError((_) => <String, dynamic>{}),
- dio
- .post<Map<String, dynamic>>('uc/asset/wallet', data: {})
- .then((r) => r.data)
- .catchError((_) => null),
- ]);
- final coinsRaw = results[0] as List<Map<String, dynamic>>;
- final spotData = results[1] as Map<String, dynamic>;
- final fundRaw = results[2];
- var openCoins = parseOpenCoinSymbols(coinsRaw);
- var selected = state.selectedCoin.toUpperCase();
- if (_options.preferDefaultSymbol &&
- openCoins.contains(_options.defaultSymbol.toUpperCase())) {
- selected = _options.defaultSymbol.toUpperCase();
- } else if (!openCoins.contains(selected)) {
- selected = openCoins.contains('USDT') ? 'USDT' : openCoins.first;
- }
- state = state.copyWith(
- openCoins: openCoins,
- selectedCoin: selected,
- spotAvailable: parseSpotAvailableMap(spotData),
- fundAvailable: parseFundWalletBalances(fundRaw),
- spotTradingUsdtBalance:
- parseSpotAvailableMap(spotData)['USDT'] ?? state.spotTradingUsdtBalance,
- isLoadingCoins: false,
- );
- } catch (e) {
- state = state.copyWith(isLoadingCoins: false, errorMessage: e.toString());
- }
- }
- Future<void> refresh() async {
- if (!_initialized) {
- return;
- }
- await _loadLegacyBalances();
- if (state.isSpotCoinTransfer || state.spotTradingBridgeOnly) {
- await _loadSpotTransferAssets();
- }
- }
- void selectCoin(String coin) {
- state = state.copyWith(selectedCoin: coin.toUpperCase());
- }
- void setFromType(String type) {
- final pair = normalizeTransferPair(type, state.toType);
- state = state.copyWith(fromType: pair.$1, toType: pair.$2);
- _maybeLoadSpotAssets();
- }
- void setToType(String type) {
- final pair = normalizeTransferPair(state.fromType, type);
- state = state.copyWith(fromType: pair.$1, toType: pair.$2);
- _maybeLoadSpotAssets();
- }
- void _maybeLoadSpotAssets() {
- if (state.isSpotCoinTransfer) {
- Future.microtask(_loadSpotTransferAssets);
- }
- }
- void swapAccounts() {
- if (state.spotTradingBridgeOnly) {
- final nextFrom =
- state.fromType == kWalletSpot ? kWalletSpotTrading : kWalletSpot;
- final nextTo =
- nextFrom == kWalletSpot ? kWalletSpotTrading : kWalletSpot;
- state = state.copyWith(fromType: nextFrom, toType: nextTo);
- Future.microtask(_loadSpotTransferAssets);
- return;
- }
- final pair = normalizeTransferPair(state.toType, state.fromType);
- state = state.copyWith(fromType: pair.$1, toType: pair.$2);
- _maybeLoadSpotAssets();
- }
- Future<bool> submit(String amount) async {
- final pair = normalizeTransferPair(state.fromType, state.toType);
- state = state.copyWith(fromType: pair.$1, toType: pair.$2);
- if (pair.$1 == pair.$2) {
- state = state.copyWith(errorMessage: 'errSameAccount');
- return false;
- }
- final input = _d(amount);
- if (input <= Decimal.zero) {
- state = state.copyWith(errorMessage: 'errEnterAmount');
- return false;
- }
- if (input > state.fromBalance) {
- state = state.copyWith(errorMessage: 'errExceedBalance');
- return false;
- }
- state = state.copyWith(isSubmitting: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- final fromWallet = state.fromType;
- final toWallet = state.toType;
- if (state.spotTradingBridgeOnly ||
- fromWallet == kWalletSpotTrading ||
- toWallet == kWalletSpotTrading) {
- final direction = fromWallet == kWalletSpot ? 1 : 2;
- await SpotService(dio).transfer(
- symbol: state.selectedCoin,
- amount: input.toDouble(),
- direction: direction,
- );
- } else {
- await AssetService(dio).transfer(
- amount: amount,
- from: fromWallet,
- to: toWallet,
- );
- }
- state = state.copyWith(isSubmitting: false);
- await refresh();
- return true;
- } catch (e) {
- state = state.copyWith(
- isSubmitting: false,
- errorMessage: extractErrorMessage(e),
- );
- return false;
- }
- }
- }
- final transferProvider =
- AutoDisposeNotifierProvider<TransferNotifier, TransferState>(
- TransferNotifier.new,
- );
- class TransferHistoryState {
- final List<TransferRecord> records;
- final bool isLoading;
- final bool hasMore;
- final int currentPage;
- final String? errorMessage;
- const TransferHistoryState({
- this.records = const [],
- this.isLoading = false,
- this.hasMore = true,
- this.currentPage = 1,
- this.errorMessage,
- });
- TransferHistoryState copyWith({
- List<TransferRecord>? records,
- bool? isLoading,
- bool? hasMore,
- int? currentPage,
- String? errorMessage,
- }) =>
- TransferHistoryState(
- records: records ?? this.records,
- isLoading: isLoading ?? this.isLoading,
- hasMore: hasMore ?? this.hasMore,
- currentPage: currentPage ?? this.currentPage,
- errorMessage: errorMessage,
- );
- }
- class TransferHistoryNotifier
- extends AutoDisposeNotifier<TransferHistoryState> {
- static const _pageSize = 10;
- @override
- TransferHistoryState build() {
- Future.microtask(_loadFirst);
- return const TransferHistoryState(isLoading: true);
- }
- Future<void> _loadFirst() async {
- state = state.copyWith(isLoading: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- final records = await AssetService(dio)
- .getTransferList(pageNo: 1, pageSize: _pageSize);
- state = state.copyWith(
- records: records,
- isLoading: false,
- currentPage: 1,
- hasMore: records.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: e.toString());
- }
- }
- Future<void> refresh() => _loadFirst();
- Future<void> loadMore() async {
- if (state.isLoading || !state.hasMore) {
- return;
- }
- state = state.copyWith(isLoading: true);
- try {
- final nextPage = state.currentPage + 1;
- final dio = ref.read(dioClientProvider);
- final records = await AssetService(dio)
- .getTransferList(pageNo: nextPage, pageSize: _pageSize);
- state = state.copyWith(
- records: [...state.records, ...records],
- isLoading: false,
- currentPage: nextPage,
- hasMore: records.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: e.toString());
- }
- }
- }
- final transferHistoryProvider =
- AutoDisposeNotifierProvider<TransferHistoryNotifier, TransferHistoryState>(
- TransferHistoryNotifier.new,
- );
|