| 1 | import 'dart:convert'; |
| 2 | import 'dart:developer'; |
| 3 | |
| 4 | import 'package:cake_wallet/entities/solana_nft_asset_model.dart'; |
| 5 | import 'package:cake_wallet/generated/i18n.dart'; |
| 6 | import 'package:cake_wallet/reactions/wallet_connect.dart'; |
| 7 | import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart'; |
| 8 | import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart'; |
| 9 | import 'package:cake_wallet/evm/evm.dart'; |
| 10 | import 'package:cake_wallet/utils/debounce.dart'; |
| 11 | import 'package:cw_core/wallet_type.dart'; |
| 12 | import 'package:cw_core/utils/proxy_wrapper.dart'; |
| 13 | import 'package:mobx/mobx.dart'; |
| 14 | import 'package:cake_wallet/.secrets.g.dart' as secrets; |
| 15 | |
| 16 | import 'package:cake_wallet/entities/wallet_nft_response.dart'; |
| 17 | import 'package:cake_wallet/store/app_store.dart'; |
| 18 | |
| 19 | part 'nft_view_model.g.dart'; |
| 20 | |
| 21 | class NFTViewModel = NFTViewModelBase with _$NFTViewModel; |
| 22 | |
| 23 | abstract class NFTViewModelBase with Store { |
| 24 | NFTViewModelBase(this.appStore, this.bottomSheetService) |
| 25 | : isLoading = false, |
| 26 | isImportNFTLoading = false, |
| 27 | nftAssetByWalletModels = ObservableList(), |
| 28 | solanaNftAssetModels = ObservableList() { |
| 29 | if (isEVMCompatibleChain(appStore.wallet!.type)) { |
| 30 | reaction((_) { |
| 31 | final wallet = appStore.wallet; |
| 32 | if (wallet != null) return wallet.chainId; |
| 33 | |
| 34 | return null; |
| 35 | }, (_) => _fetchDebounce.run(() => getNFTAssetByWallet())); |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | final AppStore appStore; |
| 40 | bool _nftErrorPopupShown = false; |
| 41 | final BottomSheetService bottomSheetService; |
| 42 | final Debounce _fetchDebounce = Debounce(const Duration(milliseconds: 500)); |
| 43 | |
| 44 | @observable |
| 45 | bool isLoading; |
| 46 | |
| 47 | @observable |
| 48 | bool isImportNFTLoading; |
| 49 | |
| 50 | ObservableList<NFTAssetModel> nftAssetByWalletModels; |
| 51 | |
| 52 | ObservableList<SolanaNFTAssetModel> solanaNftAssetModels; |
| 53 | |
| 54 | @action |
| 55 | Future<void> getNFTAssetByWallet() async { |
| 56 | final wallet = appStore.wallet!; |
| 57 | |
| 58 | if (!isNFTACtivatedChain(wallet.type, chainId: wallet.chainId)) return; |
| 59 | |
| 60 | final walletAddress = wallet.walletInfo.address; |
| 61 | log('Fetching wallet NFTs for $walletAddress'); |
| 62 | |
| 63 | final chainName = getChainNameBasedOnWalletType(wallet.type, chainId: wallet.chainId); |
| 64 | // the [chain] refers to the chain network that the nft is on |
| 65 | // the [format] refers to the number format type of the responses |
| 66 | // the [normalizedMetadata] field is a boolean that determines if |
| 67 | // the response would include a json string of the NFT Metadata that can be decoded |
| 68 | // and used within the wallet |
| 69 | // the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response. |
| 70 | |
| 71 | Uri uri; |
| 72 | if (wallet.type == WalletType.solana) { |
| 73 | uri = Uri.https( |
| 74 | 'solana-gateway.moralis.io', |
| 75 | '/account/$chainName/$walletAddress/nft', |
| 76 | ); |
| 77 | } else { |
| 78 | uri = Uri.https( |
| 79 | 'deep-index.moralis.io', |
| 80 | '/api/v2.2/$walletAddress/nft', |
| 81 | { |
| 82 | "chain": chainName, |
| 83 | "format": "decimal", |
| 84 | "media_items": "false", |
| 85 | "exclude_spam": "true", |
| 86 | "normalizeMetadata": "true", |
| 87 | }, |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | try { |
| 92 | if (isLoading) return; |
| 93 | |
| 94 | isLoading = true; |
| 95 | |
| 96 | final response = await ProxyWrapper().get( |
| 97 | clearnetUri: uri, |
| 98 | headers: { |
| 99 | "Accept": "application/json", |
| 100 | "X-API-Key": secrets.moralisApiKey, |
| 101 | }, |
| 102 | ); |
| 103 | |
| 104 | final decodedResponse = jsonDecode(response.body); |
| 105 | |
| 106 | _nftErrorPopupShown = false; |
| 107 | |
| 108 | if (wallet.type == WalletType.solana) { |
| 109 | final results = await Future.wait( |
| 110 | (decodedResponse as List<dynamic>).map( |
| 111 | (x) { |
| 112 | final data = x as Map<String, dynamic>; |
| 113 | final mint = data['mint'] as String? ?? ''; |
| 114 | return getSolanaNFTDetails(mint, chainName); |
| 115 | }, |
| 116 | ).toList(), |
| 117 | ); |
| 118 | |
| 119 | solanaNftAssetModels.clear(); |
| 120 | |
| 121 | solanaNftAssetModels.addAll(results); |
| 122 | } else { |
| 123 | final result = |
| 124 | WalletNFTsResponseModel.fromJson(decodedResponse as Map<String, dynamic>).result ?? []; |
| 125 | |
| 126 | nftAssetByWalletModels.clear(); |
| 127 | |
| 128 | nftAssetByWalletModels.addAll(result); |
| 129 | } |
| 130 | } catch (e) { |
| 131 | log(e.toString()); |
| 132 | if (!_nftErrorPopupShown) { |
| 133 | _nftErrorPopupShown = true; |
| 134 | bottomSheetService.queueBottomSheet( |
| 135 | isModalDismissible: true, |
| 136 | widget: BottomSheetMessageDisplayWidget( |
| 137 | message: S.current.moralis_nft_error, |
| 138 | ), |
| 139 | ); |
| 140 | } |
| 141 | } finally { |
| 142 | isLoading = false; |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | Future<SolanaNFTAssetModel> getSolanaNFTDetails(String address, String chainName) async { |
| 147 | final uri = Uri.https( |
| 148 | 'solana-gateway.moralis.io', |
| 149 | '/nft/$chainName/$address/metadata', |
| 150 | ); |
| 151 | |
| 152 | final response = await ProxyWrapper().get( |
| 153 | clearnetUri: uri, |
| 154 | headers: { |
| 155 | "Accept": "application/json", |
| 156 | "X-API-Key": secrets.moralisApiKey, |
| 157 | }, |
| 158 | ); |
| 159 | |
| 160 | final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>; |
| 161 | |
| 162 | return SolanaNFTAssetModel.fromJson(decodedResponse); |
| 163 | } |
| 164 | |
| 165 | @action |
| 166 | Future<void> importNFT(String tokenAddress, String? tokenId) async { |
| 167 | final walletType = appStore.wallet!.type; |
| 168 | int? chainId; |
| 169 | if (isEVMCompatibleChain(walletType)) { |
| 170 | chainId = evm!.getSelectedChainId(appStore.wallet!); |
| 171 | } |
| 172 | final chainName = getChainNameBasedOnWalletType(walletType, chainId: chainId); |
| 173 | // the [chain] refers to the chain network that the nft is on |
| 174 | // the [format] refers to the number format type of the responses |
| 175 | // the [normalizedMetadata] field is a boolean that determines if |
| 176 | // the response would include a json string of the NFT Metadata that can be decoded |
| 177 | // and used within the wallet |
| 178 | |
| 179 | try { |
| 180 | isImportNFTLoading = true; |
| 181 | |
| 182 | if (appStore.wallet!.type == WalletType.solana) { |
| 183 | final result = await getSolanaNFTDetails(tokenAddress, chainName); |
| 184 | |
| 185 | solanaNftAssetModels.add(result); |
| 186 | } else { |
| 187 | final uri = Uri.https( |
| 188 | 'deep-index.moralis.io', |
| 189 | '/api/v2.2/nft/$tokenAddress/$tokenId', |
| 190 | { |
| 191 | "chain": chainName, |
| 192 | "format": "decimal", |
| 193 | "media_items": "false", |
| 194 | "normalizeMetadata": "true", |
| 195 | }, |
| 196 | ); |
| 197 | final response = await ProxyWrapper().get( |
| 198 | clearnetUri: uri, |
| 199 | headers: { |
| 200 | "Accept": "application/json", |
| 201 | "X-API-Key": secrets.moralisApiKey, |
| 202 | }, |
| 203 | ); |
| 204 | |
| 205 | final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>; |
| 206 | |
| 207 | final nftAsset = NFTAssetModel.fromJson(decodedResponse); |
| 208 | |
| 209 | nftAssetByWalletModels.add(nftAsset); |
| 210 | } |
| 211 | } catch (e) { |
| 212 | bottomSheetService.queueBottomSheet( |
| 213 | isModalDismissible: true, |
| 214 | widget: BottomSheetMessageDisplayWidget( |
| 215 | message: e.toString(), |
| 216 | ), |
| 217 | ); |
| 218 | } finally { |
| 219 | isImportNFTLoading = false; |
| 220 | } |
| 221 | } |
| 222 | } |