import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/solana_rpc_http_service.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_solana/pending_solana_transaction.dart'; import 'package:cw_solana/solana_balance.dart'; import 'package:cw_solana/solana_exceptions.dart'; import 'package:cw_solana/solana_transaction_model.dart'; import 'package:cw_solana/spl_token.dart'; import 'package:on_chain/solana/solana.dart'; import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart'; import 'package:on_chain/solana/src/models/pda/pda.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta.dart'; import '.secrets.g.dart' as secrets; class SolanaWalletClient { // Minimum amount in SOL to consider a transaction valid (to filter spam) static const double minValidAmount = 0.00000003; final httpClient = ProxyWrapper().getHttpClient(); SolanaRPC? _provider; bool connect(Node node) { try { String formattedUrl; String protocolUsed = node.isSSL ? "https" : "http"; if (node.uriRaw == 'rpc.ankr.com') { String ankrApiKey = secrets.ankrApiKey; formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey'; } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') { String chainStackApiKey = secrets.chainStackApiKey; formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey'; } else { formattedUrl = '$protocolUsed://${node.uriRaw}'; } _provider = SolanaRPC(SolanaRPCHTTPService(url: formattedUrl)); return true; } catch (e) { return false; } } Future getBalance(String walletAddress) async { try { final balance = await _provider!.requestWithContext( SolanaRPCGetBalance( account: SolAddress(walletAddress), ), ); final balInLamp = balance.result.toDouble(); final solBalance = balInLamp / SolanaUtils.lamportsPerSol; return solBalance; } catch (_) { return 0.0; } } Future?> getSPLTokenAccounts( String mintAddress, String publicKey) async { try { final result = await _provider!.request( SolanaRPCGetTokenAccountsByOwner( account: SolAddress(publicKey), mint: SolAddress(mintAddress), commitment: Commitment.confirmed, encoding: SolanaRPCEncoding.base64, ), ); return result; } catch (e) { return null; } } Future getSplTokenBalance( String mintAddress, String walletAddress) async { // Fetch the token accounts (a token can have multiple accounts for various uses) final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress); // Handle scenario where there is no token account if (tokenAccounts == null || tokenAccounts.isEmpty) { return null; } // Sum the balances of all accounts with the specified mint address double totalBalance = 0.0; for (var tokenAccount in tokenAccounts) { final tokenAmountResult = await _provider!.request( SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey), ); final balance = tokenAmountResult.uiAmountString; final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0; totalBalance += balanceAsDouble; } return SolanaBalance(totalBalance); } Future getFeeForMessage(String message, Commitment commitment) async { try { final feeForMessage = await _provider!.request( SolanaRPCGetFeeForMessage( encodedMessage: message, commitment: commitment, ), ); final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol; return fee; } catch (_) { return 0.0; } } Future getEstimatedFee( SolanaPublicKey publicKey, Commitment commitment) async { final message = await _getMessageForNativeTransaction( publicKey: publicKey, destinationAddress: publicKey.toAddress().address, lamports: SolanaUtils.lamportsPerSol, commitment: commitment, ); final estimatedFee = await _getFeeFromCompiledMessage( message, commitment, ); return estimatedFee; } Future parseTransaction({ VersionedTransactionResponse? txResponse, required String walletAddress, String? splTokenSymbol, }) async { if (txResponse == null) return null; try { final blockTime = txResponse.blockTime; final meta = txResponse.meta; final transaction = txResponse.transaction; if (meta == null || transaction == null) return null; final int fee = meta.fee; final feeInSol = fee / SolanaUtils.lamportsPerSol; final message = transaction.message; final instructions = message.compiledInstructions; String signature = (txResponse.transaction?.signatures.isEmpty ?? true) ? "" : Base58Encoder.encode(txResponse.transaction!.signatures.first); for (final instruction in instructions) { final programId = message.accountKeys[instruction.programIdIndex]; if (programId == SystemProgramConst.programId || programId == ComputeBudgetConst.programId) { // For native solana transactions if (instruction.accounts.length < 2) continue; // Get the fee payer index based on transaction type // For legacy transfers, the first account is usually the fee payer // For versioned, the first account in instruction is usually the fee payer final feePayerIndex = txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0]; final transactionModel = await _parseNativeTransaction( message: message, meta: meta, fee: fee, feeInSol: feeInSol, feePayerIndex: feePayerIndex, walletAddress: walletAddress, signature: signature, blockTime: blockTime, ); if (transactionModel != null) { return transactionModel; } } else if (programId == SPLTokenProgramConst.tokenProgramId) { // For SPL Token transactions if (instruction.accounts.length < 2) continue; final transactionModel = await _parseSPLTokenTransaction( message: message, meta: meta, fee: fee, feeInSol: feeInSol, instruction: instruction, walletAddress: walletAddress, signature: signature, blockTime: blockTime, splTokenSymbol: splTokenSymbol, ); if (transactionModel != null) { return transactionModel; } } else if (programId == AssociatedTokenAccountProgramConst.associatedTokenProgramId) { // For ATA program, we need to check if this is a create account transaction // or if it's part of a normal token transfer // We skip this transaction if this is the only instruction (this means that it's a create account transaction) if (instructions.length == 1) { return null; } // We look for a token transfer instruction in the same transaction bool hasTokenTransfer = false; for (final otherInstruction in instructions) { final otherProgramId = message.accountKeys[otherInstruction.programIdIndex]; if (otherProgramId == SPLTokenProgramConst.tokenProgramId) { hasTokenTransfer = true; break; } } // If there's no token transfer instruction, it means this is just an ATA creation transaction if (!hasTokenTransfer) { return null; } continue; } else { return null; } } } catch (e, s) { printV("Error parsing transaction: $e\n$s"); } return null; } Future _parseNativeTransaction({ required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, required double feeInSol, required int feePayerIndex, required String walletAddress, required String signature, required BigInt? blockTime, }) async { // Calculate total balance changes across all accounts BigInt totalBalanceChange = BigInt.zero; String? sender; String? receiver; for (int i = 0; i < meta.preBalances.length; i++) { final preBalance = meta.preBalances[i]; final postBalance = meta.postBalances[i]; final balanceChange = preBalance - postBalance; if (balanceChange > BigInt.zero) { // This account sent funds sender = message.accountKeys[i].address; totalBalanceChange += balanceChange; } else if (balanceChange < BigInt.zero) { // This account received funds receiver = message.accountKeys[i].address; } } // We subtract the fee from total balance change if the fee payer is the sender if (sender == message.accountKeys[feePayerIndex].address) { totalBalanceChange -= BigInt.from(fee); } if (sender == null || receiver == null) { return null; } final amount = totalBalanceChange / BigInt.from(1e9); final amountInSol = amount.abs().toDouble(); // Skip transactions with very small amounts (likely spam) if (amountInSol < minValidAmount) { return null; } return SolanaTransactionModel( isOutgoingTx: sender == walletAddress, from: sender, to: receiver, id: signature, amount: amountInSol, programId: SystemProgramConst.programId.address, tokenSymbol: 'SOL', blockTimeInInt: blockTime?.toInt() ?? 0, fee: feeInSol, ); } Future _parseSPLTokenTransaction({ required VersionedMessage message, required ConfirmedTransactionMeta meta, required int fee, required double feeInSol, required CompiledInstruction instruction, required String walletAddress, required String signature, required BigInt? blockTime, String? splTokenSymbol, }) async { final preBalances = meta.preTokenBalances; final postBalances = meta.postTokenBalances; double amount = 0.0; bool isOutgoing = false; String? mintAddress; double userPreAmount = 0.0; if (preBalances != null && preBalances.isNotEmpty) { for (final preBal in preBalances) { if (preBal.owner?.address == walletAddress) { userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0; mintAddress = preBal.mint.address; break; } } } double userPostAmount = 0.0; if (postBalances != null && postBalances.isNotEmpty) { for (final postBal in postBalances) { if (postBal.owner?.address == walletAddress) { userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0; mintAddress ??= postBal.mint.address; break; } } } final diff = userPreAmount - userPostAmount; final rawAmount = diff.abs(); final amountInString = rawAmount.toStringAsFixed(6); amount = double.parse(amountInString); isOutgoing = diff > 0; if (mintAddress == null && instruction.accounts.length >= 4) { final mintIndex = instruction.accounts[3]; mintAddress = message.accountKeys[mintIndex].address; } final sender = message.accountKeys[instruction.accounts[0]].address; final receiver = message.accountKeys[instruction.accounts[1]].address; String? tokenSymbol = splTokenSymbol; if (tokenSymbol == null && mintAddress != null) { final token = await getTokenInfo(mintAddress); tokenSymbol = token?.symbol; } return SolanaTransactionModel( isOutgoingTx: isOutgoing, from: sender, to: receiver, id: signature, amount: amount, programId: SPLTokenProgramConst.tokenProgramId.address, blockTimeInInt: blockTime?.toInt() ?? 0, tokenSymbol: tokenSymbol ?? '', fee: feeInSol, ); } /// Load the Address's transactions into the account Future> fetchTransactions( SolAddress address, { String? splTokenSymbol, int? splTokenDecimal, Commitment? commitment, SolAddress? walletAddress, required void Function(List) onUpdate, }) async { List transactions = []; try { final signatures = await _provider!.request( SolanaRPCGetSignaturesForAddress( account: address, commitment: commitment, ), ); // The maximum concurrent batch size. const int batchSize = 10; for (int i = 0; i < signatures.length; i += batchSize) { final batch = signatures.skip(i).take(batchSize).toList(); final batchResponses = await Future.wait(batch.map((signature) async { try { return await _provider!.request( SolanaRPCGetTransaction( transactionSignature: signature['signature'], encoding: SolanaRPCEncoding.jsonParsed, maxSupportedTransactionVersion: 0, ), ); } catch (e) { // printV("Error fetching transaction: $e"); return null; } })); final versionedBatchResponses = batchResponses.whereType(); final parsedTransactionsFutures = versionedBatchResponses.map((tx) => parseTransaction( txResponse: tx, splTokenSymbol: splTokenSymbol, walletAddress: walletAddress?.address ?? address.address, )); final parsedTransactions = await Future.wait(parsedTransactionsFutures); transactions.addAll( parsedTransactions.whereType().toList()); // Only update UI if we have new valid transactions if (parsedTransactions.isNotEmpty) { onUpdate(List.from(transactions)); } if (i + batchSize < signatures.length) { await Future.delayed(const Duration(milliseconds: 300)); } } return transactions; } catch (err, s) { printV('Error fetching transactions: $err \n$s'); return []; } } Future> getSPLTokenTransfers({ required String mintAddress, required String splTokenSymbol, required int splTokenDecimal, required SolanaPrivateKey privateKey, required void Function(List) onUpdate, }) async { ProgramDerivedAddress? associatedTokenAccount; final ownerWalletAddress = privateKey.publicKey().toAddress(); try { associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( payerPrivateKey: privateKey, mintAddress: SolAddress(mintAddress), ownerAddress: ownerWalletAddress, shouldCreateATA: false, ); } catch (e, s) { printV('$e \n $s'); } if (associatedTokenAccount == null) return []; final accountPublicKey = associatedTokenAccount.address; final tokenTransactions = await fetchTransactions( accountPublicKey, splTokenSymbol: splTokenSymbol, splTokenDecimal: splTokenDecimal, walletAddress: ownerWalletAddress, onUpdate: onUpdate, ); return tokenTransactions; } final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { if (tokenInfoCache.containsKey(mintAddress)) { return tokenInfoCache[mintAddress]; } else { final token = await fetchSPLTokenInfo(mintAddress); if (token != null) { tokenInfoCache[mintAddress] = token; } return token; } } Future fetchSPLTokenInfo(String mintAddress) async { final programAddress = MetaplexTokenMetaDataProgramUtils.findMetadataPda( mint: SolAddress(mintAddress)); final token = await _provider!.request( SolanaRPCGetMetadataAccount( account: programAddress.address, commitment: Commitment.confirmed, ), ); if (token == null) { return null; } final metadata = token.data; String? iconPath; //TODO(Further explore fetching images) // try { // iconPath = await _client.getIconImageFromTokenUri(metadata.uri); // } catch (_) {} String filteredTokenSymbol = metadata.symbol .replaceFirst(RegExp('^\\\$'), '') .replaceAll('\u0000', ''); return SPLToken.fromMetadata( name: metadata.name, mint: metadata.symbol, symbol: filteredTokenSymbol, mintAddress: token.mint.address, iconPath: iconPath, ); } void stop() {} SolanaRPC? get getSolanaProvider => _provider; Future signSolanaTransaction({ required String tokenTitle, required int tokenDecimals, required double inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required bool isSendAll, required double solBalance, String? tokenMint, List references = const [], }) async { const commitment = Commitment.confirmed; if (tokenTitle == CryptoCurrency.sol.title) { final pendingNativeTokenTransaction = await _signNativeTokenTransaction( inputAmount: inputAmount, destinationAddress: destinationAddress, ownerPrivateKey: ownerPrivateKey, commitment: commitment, isSendAll: isSendAll, solBalance: solBalance, ); return pendingNativeTokenTransaction; } else { final pendingSPLTokenTransaction = _signSPLTokenTransaction( tokenDecimals: tokenDecimals, tokenMint: tokenMint!, inputAmount: inputAmount, ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, commitment: commitment, solBalance: solBalance, ); return pendingSPLTokenTransaction; } } Future _getLatestBlockhash(Commitment commitment) async { final latestBlockhash = await _provider!.request( const SolanaRPCGetLatestBlockhash(), ); return latestBlockhash.blockhash; } Future _getMessageForNativeTransaction({ required SolanaPublicKey publicKey, required String destinationAddress, required int lamports, required Commitment commitment, }) async { final instructions = [ SystemProgram.transfer( from: publicKey.toAddress(), layout: SystemTransferLayout(lamports: BigInt.from(lamports)), to: SolAddress(destinationAddress), ), ]; final latestBlockhash = await _getLatestBlockhash(commitment); final message = Message.compile( transactionInstructions: instructions, payer: publicKey.toAddress(), recentBlockhash: latestBlockhash, ); return message; } Future _getMessageForSPLTokenTransaction({ required SolAddress ownerAddress, required SolAddress destinationAddress, required int tokenDecimals, required SolAddress mintAddress, required SolAddress sourceAccount, required int amount, required Commitment commitment, }) async { final instructions = [ SPLTokenProgram.transferChecked( layout: SPLTokenTransferCheckedLayout( amount: BigInt.from(amount), decimals: tokenDecimals, ), mint: mintAddress, source: sourceAccount, destination: destinationAddress, owner: ownerAddress, ) ]; final latestBlockhash = await _getLatestBlockhash(commitment); final message = Message.compile( transactionInstructions: instructions, payer: ownerAddress, recentBlockhash: latestBlockhash, ); return message; } Future _getFeeFromCompiledMessage( Message message, Commitment commitment) async { final base64Message = base64Encode(message.serialize()); final fee = await getFeeForMessage(base64Message, commitment); return fee; } Future hasSufficientFundsLeftForRent({ required double inputAmount, required double solBalance, required double fee, }) async { final rent = await _provider!.request( SolanaRPCGetMinimumBalanceForRentExemption( size: SolanaTokenAccountUtils.accountSize, ), ); final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble(); final remnant = solBalance - (inputAmount + fee); if (remnant > rentInSol) return true; return false; } Future _signNativeTokenTransaction({ required double inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required bool isSendAll, required double solBalance, }) async { // Convert SOL to lamport int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt(); Message message = await _getMessageForNativeTransaction( publicKey: ownerPrivateKey.publicKey(), destinationAddress: destinationAddress, lamports: lamports, commitment: commitment, ); SolAddress latestBlockhash = await _getLatestBlockhash(commitment); final fee = await _getFeeFromCompiledMessage( message, commitment, ); bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( inputAmount: inputAmount, fee: fee, solBalance: solBalance, ); if (!hasSufficientFundsLeft) { throw SolanaSignNativeTokenTransactionRentException(); } String serializedTransaction; if (isSendAll) { final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt(); final updatedLamports = lamports - feeInLamports; final transaction = _constructNativeTransaction( ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, lamports: updatedLamports, ); serializedTransaction = await _signTransactionInternal( ownerPrivateKey: ownerPrivateKey, transaction: transaction, ); } else { final transaction = _constructNativeTransaction( ownerPrivateKey: ownerPrivateKey, destinationAddress: destinationAddress, latestBlockhash: latestBlockhash, lamports: lamports, ); serializedTransaction = await _signTransactionInternal( ownerPrivateKey: ownerPrivateKey, transaction: transaction, ); } sendTx() async => await sendTransaction( serializedTransaction: serializedTransaction, commitment: commitment, ); final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, ); return pendingTransaction; } SolanaTransaction _constructNativeTransaction({ required SolanaPrivateKey ownerPrivateKey, required String destinationAddress, required SolAddress latestBlockhash, required int lamports, }) { final owner = ownerPrivateKey.publicKey().toAddress(); /// Create a transfer instruction to move funds from the owner to the receiver. final transferInstruction = SystemProgram.transfer( from: owner, layout: SystemTransferLayout(lamports: BigInt.from(lamports)), to: SolAddress(destinationAddress), ); /// Construct a Solana transaction with the transfer instruction. return SolanaTransaction( instructions: [transferInstruction], recentBlockhash: latestBlockhash, payerKey: ownerPrivateKey.publicKey().toAddress(), type: TransactionType.v0, ); } Future _getOrCreateAssociatedTokenAccount({ required SolanaPrivateKey payerPrivateKey, required SolAddress ownerAddress, required SolAddress mintAddress, required bool shouldCreateATA, }) async { final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerAddress, ); SolanaAccountInfo? accountInfo; try { accountInfo = await _provider!.request( SolanaRPCGetAccountInfo( account: associatedTokenAccount.address, commitment: Commitment.confirmed, ), ); } catch (e) { accountInfo = null; } // If account exists, we return the associated token account if (accountInfo != null) return associatedTokenAccount; if (!shouldCreateATA) return null; final payerAddress = payerPrivateKey.publicKey().toAddress(); final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount( payer: payerAddress, associatedToken: associatedTokenAccount.address, owner: ownerAddress, mint: mintAddress, ); final blockhash = await _getLatestBlockhash(Commitment.confirmed); final transaction = SolanaTransaction( payerKey: payerAddress, instructions: [createAssociatedTokenAccount], recentBlockhash: blockhash, type: TransactionType.v0, ); final serializedTransaction = await _signTransactionInternal( ownerPrivateKey: payerPrivateKey, transaction: transaction, ); await sendTransaction( serializedTransaction: serializedTransaction, commitment: Commitment.confirmed, ); // Wait for confirmation await Future.delayed(const Duration(seconds: 2)); return associatedTokenAccount; } Future _signSPLTokenTransaction({ required int tokenDecimals, required String tokenMint, required double inputAmount, required String destinationAddress, required SolanaPrivateKey ownerPrivateKey, required Commitment commitment, required double solBalance, }) async { final mintAddress = SolAddress(tokenMint); // Input by the user final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt(); ProgramDerivedAddress? associatedSenderAccount; try { associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount( mint: mintAddress, owner: ownerPrivateKey.publicKey().toAddress(), ); } catch (e) { associatedSenderAccount = null; } // Throw an appropriate exception if the sender has no associated // token account if (associatedSenderAccount == null) { throw SolanaNoAssociatedTokenAccountException( ownerPrivateKey.publicKey().toAddress().address, mintAddress.address, ); } ProgramDerivedAddress? associatedRecipientAccount; try { associatedRecipientAccount = await _getOrCreateAssociatedTokenAccount( payerPrivateKey: ownerPrivateKey, mintAddress: mintAddress, ownerAddress: SolAddress(destinationAddress), shouldCreateATA: true, ); } catch (e) { associatedRecipientAccount = null; throw SolanaCreateAssociatedTokenAccountException(e.toString()); } if (associatedRecipientAccount == null) { throw SolanaCreateAssociatedTokenAccountException( 'Error fetching recipient associated token account', ); } final transferInstructions = SPLTokenProgram.transferChecked( layout: SPLTokenTransferCheckedLayout( amount: BigInt.from(amount), decimals: tokenDecimals, ), mint: mintAddress, source: associatedSenderAccount.address, destination: associatedRecipientAccount.address, owner: ownerPrivateKey.publicKey().toAddress(), ); final latestBlockHash = await _getLatestBlockhash(commitment); final transaction = SolanaTransaction( payerKey: ownerPrivateKey.publicKey().toAddress(), instructions: [transferInstructions], recentBlockhash: latestBlockHash, ); final message = await _getMessageForSPLTokenTransaction( ownerAddress: ownerPrivateKey.publicKey().toAddress(), tokenDecimals: tokenDecimals, mintAddress: mintAddress, destinationAddress: associatedRecipientAccount.address, sourceAccount: associatedSenderAccount.address, amount: amount, commitment: commitment, ); final fee = await _getFeeFromCompiledMessage(message, commitment); bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent( inputAmount: 0, fee: fee, solBalance: solBalance, ); if (!hasSufficientFundsLeft) { throw SolanaSignSPLTokenTransactionRentException(); } final serializedTransaction = await _signTransactionInternal( ownerPrivateKey: ownerPrivateKey, transaction: transaction, ); sendTx() async => await sendTransaction( serializedTransaction: serializedTransaction, commitment: commitment, ); final pendingTransaction = PendingSolanaTransaction( amount: inputAmount, serializedTransaction: serializedTransaction, destinationAddress: destinationAddress, sendTransaction: sendTx, fee: fee, ); return pendingTransaction; } Future _signTransactionInternal({ required SolanaPrivateKey ownerPrivateKey, required SolanaTransaction transaction, }) async { /// Sign the transaction with the owner's private key. final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage()); transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature); /// Serialize the transaction. final serializedTransaction = transaction.serializeString(); return serializedTransaction; } Future sendTransaction({ required String serializedTransaction, required Commitment commitment, }) async { try { /// Send the transaction to the Solana network. final signature = await _provider!.request( SolanaRPCSendTransaction( encodedTransaction: serializedTransaction, commitment: commitment, ), ); return signature; } catch (e) { throw Exception(e); } } Future getIconImageFromTokenUri(String uri) async { if (uri.isEmpty || uri == '…') return null; try { final client = ProxyWrapper().getHttpIOClient(); final response = await client.get(Uri.parse(uri)); final jsonResponse = json.decode(response.body) as Map; if (response.statusCode >= 200 && response.statusCode < 300) { return jsonResponse['image']; } else { return null; } } catch (e) { printV('Error occurred while fetching token image: \n${e.toString()}'); return null; } } }