dev
dart 452 lines 14 KB
Raw
1 part of 'solana.dart';
2
3 class CWSolana extends Solana {
4 @override
5 List<String> getSolanaWordList(String language) => SolanaMnemonics.englishWordlist;
6
7 WalletService createSolanaWalletService(bool isDirect) => SolanaWalletService(isDirect);
8
9 @override
10 WalletCredentials createSolanaNewWalletCredentials({
11 required String name,
12 String? mnemonic,
13 WalletInfo? walletInfo,
14 String? password,
15 String? passphrase,
16 }) =>
17 SolanaNewWalletCredentials(
18 name: name,
19 walletInfo: walletInfo,
20 password: password,
21 mnemonic: mnemonic,
22 passphrase: passphrase,
23 );
24
25 @override
26 WalletCredentials createSolanaRestoreWalletFromSeedCredentials({
27 required String name,
28 required String mnemonic,
29 required String password,
30 String? passphrase,
31 }) =>
32 SolanaRestoreWalletFromSeedCredentials(
33 name: name,
34 password: password,
35 mnemonic: mnemonic,
36 passphrase: passphrase,
37 );
38
39 @override
40 WalletCredentials createSolanaRestoreWalletFromPrivateKey({
41 required String name,
42 required String privateKey,
43 required String password,
44 }) =>
45 SolanaRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
46
47 @override
48 String getAddress(WalletBase wallet) => (wallet as SolanaWallet).walletAddresses.address;
49
50 @override
51 String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
52
53 @override
54 String getPublicKey(WalletBase wallet) =>
55 (wallet as SolanaWallet).solanaPublicKey.toAddress().address;
56
57 @override
58 Object createSolanaTransactionCredentials(
59 List<Output> outputs, {
60 required CryptoCurrency currency,
61 }) =>
62 SolanaTransactionCredentials(
63 outputs
64 .map((out) => OutputInfo(
65 fiatAmount: out.fiatAmount,
66 cryptoAmount: out.cryptoAmountMoney,
67 address: out.address,
68 note: out.note,
69 sendAll: out.sendAll,
70 extractedAddress: out.extractedAddress,
71 isParsedAddress: out.isParsedAddress,
72 ))
73 .toList(),
74 currency: currency,
75 );
76
77 Object createSolanaTransactionCredentialsRaw(
78 List<OutputInfo> outputs, {
79 required CryptoCurrency currency,
80 }) =>
81 SolanaTransactionCredentials(outputs, currency: currency);
82
83 @override
84 List<SPLToken> getSPLTokenCurrencies(WalletBase wallet) {
85 final solanaWallet = wallet as SolanaWallet;
86 return solanaWallet.splTokenCurrencies;
87 }
88
89 @override
90 Future<void> addSPLToken(
91 WalletBase wallet,
92 CryptoCurrency token,
93 String contractAddress,
94 ) async {
95 final splToken = SPLToken(
96 name: token.name,
97 symbol: token.title,
98 mintAddress: contractAddress,
99 decimal: token.decimals,
100 mint: token.name.toUpperCase(),
101 enabled: token.enabled,
102 iconPath: token.iconPath,
103 isPotentialScam: token.isPotentialScam,
104 );
105
106 await (wallet as SolanaWallet).addSPLToken(splToken);
107 }
108
109 @override
110 Future<void> deleteSPLToken(WalletBase wallet, CryptoCurrency token) async =>
111 await (wallet as SolanaWallet).deleteSPLToken(token as SPLToken);
112
113 @override
114 Future<SPLToken?> getSPLToken(WalletBase wallet, String mintAddress) async {
115 final solanaWallet = wallet as SolanaWallet;
116 return await solanaWallet.getSPLToken(mintAddress);
117 }
118
119 @override
120 Future<bool?> isTokenVerifiedOnJupiter(WalletBase wallet, String mintAddress) {
121 final solanaWallet = wallet as SolanaWallet;
122 return solanaWallet.isTokenVerifiedOnJupiter(mintAddress);
123 }
124
125 @override
126 CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
127 if (transaction.amount.currency.symbol == CryptoCurrency.sol.symbol) {
128 return CryptoCurrency.sol;
129 }
130
131 final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol);
132
133 if (token != null) {
134 return token;
135 }
136
137 return transaction.amount.currency as CryptoCurrency;
138 }
139
140 @override
141 String getTokenAddress(CryptoCurrency asset) {
142 // If it's already an SPLToken, use its mint address
143 if (asset is SPLToken) return asset.mintAddress;
144
145 // If it's not an SPLToken but has SOL tag, try to find matching SPLToken
146 if (asset.tag == 'SOL') {
147 final symbol = asset.title.toUpperCase();
148
149 // Search through default tokens to find matching symbol
150 final defaultTokens = DefaultSPLTokens().initialSPLTokens;
151 try {
152 final matchingToken = defaultTokens.firstWhere(
153 (token) => token.symbol.toUpperCase() == symbol,
154 );
155 return matchingToken.mintAddress;
156 } catch (_) {
157 // Token not found in default tokens
158 }
159 }
160
161 // Fallback - try to cast (will throw if not SPLToken)
162 return (asset as SPLToken).mintAddress;
163 }
164
165 @override
166 List<int>? getValidationLength(CryptoCurrency type) {
167 if (type is SPLToken) {
168 return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44];
169 }
170
171 return null;
172 }
173
174 @override
175 Money? getEstimateFees(WalletBase wallet) => (wallet as SolanaWallet).estimatedFee;
176
177 @override
178 List<SPLToken> getDefaultSPLTokens() => DefaultSPLTokens().initialSPLTokens;
179
180 @override
181 List<String> getDefaultTokenContractAddresses() {
182 return DefaultSPLTokens().initialSPLTokens.map((e) => e.mintAddress).toList();
183 }
184
185 @override
186 List<String> getDefaultTokenSymbols() {
187 return DefaultSPLTokens().initialSPLTokens.map((e) => e.symbol.toUpperCase()).toList();
188 }
189
190 @override
191 bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress) {
192 final solanaWallet = wallet as SolanaWallet;
193 return solanaWallet.splTokenCurrencies.any((element) => element.mintAddress == contractAddress);
194 }
195
196 @override
197 Future<PendingTransaction> signAndPrepareJupiterSwapTransaction(
198 WalletBase wallet,
199 String base64Transaction,
200 String requestId,
201 String destinationAddress,
202 Money amount,
203 Money fee,
204 ) async {
205 final solanaWallet = wallet as SolanaWallet;
206 final privateKey = solanaWallet.solanaPrivateKey;
207 final solanaProvider = solanaWallet.solanaProvider;
208
209 if (solanaProvider == null) {
210 throw Exception('Solana provider not available');
211 }
212
213 final unsignedTransactionBytes = base64.decode(base64Transaction);
214 final unsignedTransaction = SolanaTransaction.deserialize(unsignedTransactionBytes);
215
216 final signedMessage = privateKey.sign(unsignedTransaction.serializeMessage());
217 unsignedTransaction.addSignature(privateKey.publicKey().toAddress(), signedMessage);
218
219 final signedTransactionBytes = unsignedTransaction.serialize();
220 final signedTransactionBase64 = base64.encode(Uint8List.fromList(signedTransactionBytes));
221
222 Future<String> sendTx() async {
223 try {
224 if (signedTransactionBase64.isEmpty) {
225 throw Exception('Invalid transaction: transaction is empty');
226 }
227
228 if (requestId.isEmpty) {
229 throw Exception('Invalid requestId: requestId is empty');
230 }
231
232 final jupiterProvider = JupiterExchangeProvider();
233
234 final executeResponse = await jupiterProvider.executeSwap(
235 signedTransaction: signedTransactionBase64,
236 requestId: requestId,
237 );
238
239 final status = executeResponse['status'] as String?;
240 final signature = executeResponse['signature'] as String?;
241 final errorCode = executeResponse['code'] as num?;
242 final errorMessage = executeResponse['error'] as String? ?? 'Unknown error';
243
244 // Handle different status cases
245 switch (status) {
246 case 'Success':
247 if (signature == null ||
248 signature.isEmpty ||
249 signature == '1111111111111111111111111111111111111111111111111111111111111111') {
250 throw Exception(
251 'Invalid transaction signature received from Jupiter. '
252 'Status: $status',
253 );
254 }
255 return signature;
256 case 'Failed':
257 String userFriendlyError = _getJupiterErrorMessage(errorCode, errorMessage);
258 // Even when failed, Jupiter may return a signature for solscan
259 if (signature != null && signature.isNotEmpty) {
260 throw JupiterSwapFailedException(
261 message: userFriendlyError,
262 signature: signature,
263 errorCode: errorCode,
264 errorMessage: errorMessage,
265 );
266 } else {
267 throw Exception(userFriendlyError);
268 }
269 case 'Pending':
270 case 'Processing':
271 throw Exception(
272 'Jupiter swap is still processing. Please wait and try checking the transaction status.',
273 );
274 default:
275 throw Exception(
276 'Jupiter swap returned unknown status: $status. Error: $errorMessage. Code: $errorCode',
277 );
278 }
279 } on JupiterSwapFailedException {
280 rethrow;
281 } catch (e) {
282 throw Exception('Failed to execute Jupiter swap: $e');
283 }
284 }
285
286 return PendingSolanaTransaction(
287 amount: amount,
288 serializedTransaction: signedTransactionBase64,
289 destinationAddress: destinationAddress,
290 sendTransaction: sendTx,
291 fee: fee,
292 );
293 }
294
295 /// Get user-friendly error message based on Jupiter error code
296 String _getJupiterErrorMessage(num? errorCode, String errorMessage) {
297 if (errorCode == null) {
298 return 'Jupiter swap failed: $errorMessage';
299 }
300
301 switch (errorCode.toInt()) {
302 case -2000:
303 return 'Transaction failed to land on the network. Please try again.';
304 case -2001:
305 return 'Unknown error occurred. Please try again.';
306 case -2002:
307 return 'Invalid transaction. Please try creating a new swap.';
308 case -2003:
309 return 'Quote expired. The swap quote is no longer valid. Please create a new swap.';
310 case -2004:
311 return 'Swap was rejected. This may be due to:\n'
312 '- Insufficient funds for the swap or fees\n'
313 '- Slippage tolerance exceeded (price moved too much)\n'
314 '- Network congestion\n'
315 'Please check your balance and try again with a new quote.';
316 case -2005:
317 return 'Internal error occurred. Please try again.';
318 default:
319 // Check for common program errors
320 if (errorMessage.contains('SlippageToleranceExceeded') ||
321 errorMessage.contains('slippage')) {
322 return 'Slippage tolerance exceeded. The price moved too much during the swap. '
323 'Please try again with a new quote.';
324 }
325
326 if (errorMessage.contains('InsufficientFunds') || errorMessage.contains('insufficient')) {
327 return 'Insufficient funds. Please ensure you have enough SOL for the swap and fees.';
328 }
329
330 if (errorMessage.contains('Blockhash') || errorMessage.contains('expired')) {
331 return 'Transaction expired. Please create a new swap.';
332 }
333
334 return 'Jupiter swap failed (code: $errorCode): $errorMessage. Please try again.';
335 }
336 }
337
338 @override
339 Future<void> pollForTransaction(
340 WalletBase wallet,
341 String signature, {
342 Duration initialDelay = const Duration(seconds: 1),
343 int maxRetries = 5,
344 }) async {
345 final solanaWallet = wallet as SolanaWallet;
346 await solanaWallet.pollForTransaction(
347 signature: signature,
348 initialDelay: initialDelay,
349 maxRetries: maxRetries,
350 );
351 }
352
353 @override
354 Future<void> updateTokenBalances(
355 WalletBase wallet, {
356 List<String>? tokenMints,
357 }) async {
358 final solanaWallet = wallet as SolanaWallet;
359 await solanaWallet.updateTokenBalance(tokenMints: tokenMints);
360 }
361
362 static const _minTokenUsdValue = 0.1;
363
364 Future<({double usdValue, bool hasValidFiatPrice})> _getTokenUsdValueAndFiatCheck(
365 SPLToken token,
366 double balance,
367 ) async {
368 try {
369 final settingsStore = getIt.get<SettingsStore>();
370 final torOnly = settingsStore.fiatApiMode == FiatApiMode.torOnly;
371
372 final price = await FiatConversionService.fetchPrice(
373 crypto: token,
374 fiat: FiatCurrency.usd,
375 torOnly: torOnly,
376 );
377
378 final hasValidFiatPrice = price > 0;
379 final usdValue = balance * price;
380
381 return (usdValue: usdValue, hasValidFiatPrice: hasValidFiatPrice);
382 } catch (e) {
383 return (usdValue: 0.0, hasValidFiatPrice: false);
384 }
385 }
386
387 @override
388 Future<void> discoverAndAddWalletTokens(WalletBase wallet) async {
389 if (wallet is! SolanaWallet) return;
390
391 try {
392 final result = await wallet.discoverTokensFromMoralis();
393
394 if (result.newTokens.isEmpty) return;
395
396 final List<Future<void>> tokenChecks = [];
397
398 for (final item in result.newTokens) {
399 tokenChecks.add((() async {
400 final token = item.token;
401
402 final isPropertiesSuspicious = wallet.isTokenPropertiesSuspicious(token);
403
404 final fiatResult = await _getTokenUsdValueAndFiatCheck(token, item.balance);
405
406 final isSpam = isPropertiesSuspicious || !fiatResult.hasValidFiatPrice;
407
408 token.isPotentialScam = isSpam;
409 token.enabled = (fiatResult.usdValue >= _minTokenUsdValue) && !isSpam;
410
411 await wallet.addSPLToken(token);
412 })());
413 }
414
415 await Future.wait(tokenChecks);
416
417 final discoveredMints = result.newTokens
418 .where((item) => item.token.enabled)
419 .map((item) => item.token.mintAddress)
420 .toList();
421
422 if (discoveredMints.isNotEmpty) {
423 await wallet.updateSPLTokenTransactions(specificMints: discoveredMints);
424 }
425 } catch (e) {
426 printV("Error discovering wallet tokens: ${e.toString()}");
427 }
428 }
429
430 @override
431 TransactionInfo getTransactionInfo({
432 required String id,
433 required DateTime blockTime,
434 required String to,
435 required String from,
436 String? tokenSymbol,
437 required TransactionDirection direction,
438 required Money amount,
439 required bool isPending,
440 required Money fee,
441 }) =>
442 SolanaTransactionInfo(
443 id: id,
444 date: blockTime,
445 to: to,
446 from: from,
447 direction: direction,
448 amount: amount,
449 isPending: isPending,
450 fee: fee,
451 );
452 }