Solana Wallet New Implementation (#2011)

* Feat: Implement Solana wallet using on_chain * v4.23.0 release candidate (#1974) * v4.23.0 release candidate * - Fix restoring zano from QR - Fix Zano confirmations count - Fix birdpay - Fix balance display * Fix Zano assets showing amount before they are added * - handle fetching token data while the API is busy - potential fix for duplicate transactions * fix receive confirmations, maybe * revert onChangeWallet cleanup * Fix confirmations not updating * improve zano wallet opening, fix CI commands and messages on slack (#1979) Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Cache wallet when creating/restoring as well * - hardcode Trocador Maximum limit for Zano temporarily - Configure Cake Zano node to use SSL * reformatting [skip ci] * revert to non-ssl * update build numbers [skip ci] * disable zano for desktop [skip ci] --------- Co-authored-by: cyan <cyjan@mrcyjanek.net> * CW-711 passphrase for XMR/WOWcreation (#1992) * add monero passphrase add wownero passphrase add passphrase to seed screen * obscure passphrase by default disable passphrase create for zano * Update lib/view_model/wallet_keys_view_model.dart [skip ci] * Update lib/src/screens/wallet_keys/wallet_keys_page.dart [skip ci] * Update lib/view_model/advanced_privacy_settings_view_model.dart * dynamic passphrase icon * fix polyseed not being encrypted by passphrase --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * show Zano keys properly in the keys tab (#2004) * fix: Switch private key hex encoding * Modified existing implementation to use older version of packages * fix: Fetch direct transaction history amounts instead of decimals, and add Create Account Instructions to Transaction History List * fix: Remove Create Account entries in Transaction History and disable activating token accounts of selected tokens * feat: Add passphrase support to Solana * fix: Issues with transaction amount and dissappearing transaction history items (very annoying bug) * fix: Issue with flipping transactions and incorrect transaction status * PR Review fixes --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> Co-authored-by: cyan <cyjan@mrcyjanek.net>

David Adegoke committed Mar 14, 2025 at 15:42 UTC 1b5be705f6d5f11f4508116ba9558e3178868d7d
19 files changed +709 -554
cw_core/lib/solana_rpc_http_service.dart new
+26
@@ -0,0 +1,26 @@
1 +import 'dart:convert';
2 +import 'package:http/http.dart';
3 +import 'package:on_chain/solana/solana.dart';
4 +
5 +class SolanaRPCHTTPService implements SolanaJSONRPCService {
6 + SolanaRPCHTTPService(
7 + {required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)})
8 + : client = client ?? Client();
9 + @override
10 + final String url;
11 + final Client client;
12 + final Duration defaultRequestTimeout;
13 +
14 + @override
15 + Future<Map<String, dynamic>> call(SolanaRequestDetails params, [Duration? timeout]) async {
16 + final response = await client.post(
17 + Uri.parse(url),
18 + body: params.toRequestBody(),
19 + headers: {
20 + 'Content-Type': 'application/json',
21 + },
22 + ).timeout(timeout ?? defaultRequestTimeout);
23 + final data = json.decode(response.body) as Map<String, dynamic>;
24 + return data;
25 + }
26 +}
cw_core/pubspec.yaml
+4
@@ -27,6 +27,10 @@ dependencies:
27 version: 1.0.0
28 socks5_proxy: ^1.0.4
29 unorm_dart: ^0.3.0
30 + on_chain:
31 + git:
32 + url: https://github.com/cake-tech/on_chain.git
33 + ref: cake-update-v2
34 # tor:
35 # git:
36 # url: https://github.com/cake-tech/tor.git
cw_solana/lib/default_spl_tokens.dart
+2 -2
@@ -26,7 +26,7 @@ class DefaultSPLTokens {
26 decimal: 5,
27 mint: 'Bonk',
28 iconPath: 'assets/images/bonk_icon.png',
29 - enabled: true,
29 + enabled: false,
30 ),
31 SPLToken(
32 name: 'Raydium',
@@ -35,7 +35,7 @@ class DefaultSPLTokens {
35 decimal: 6,
36 mint: 'ray',
37 iconPath: 'assets/images/ray_icon.png',
38 - enabled: true,
38 + enabled: false,
39 ),
40 SPLToken(
41 name: 'Wrapped Ethereum (Sollet)',
cw_solana/lib/pending_solana_transaction.dart
+3 -4
@@ -1,9 +1,8 @@
1 import 'package:cw_core/pending_transaction.dart';
2 -import 'package:solana/encoder.dart';
2
3 class PendingSolanaTransaction with PendingTransaction {
4 final double amount;
6 - final SignedTx signedTransaction;
5 + final String serializedTransaction;
6 final String destinationAddress;
7 final Function sendTransaction;
8 final double fee;
@@ -11,7 +10,7 @@ class PendingSolanaTransaction with PendingTransaction {
10 PendingSolanaTransaction({
11 required this.fee,
12 required this.amount,
14 - required this.signedTransaction,
13 + required this.serializedTransaction,
14 required this.destinationAddress,
15 required this.sendTransaction,
16 });
@@ -36,7 +35,7 @@ class PendingSolanaTransaction with PendingTransaction {
35 String get feeFormatted => fee.toString();
36
37 @override
39 - String get hex => signedTransaction.encode();
38 + String get hex => serializedTransaction;
39
40 @override
41 String get id => '';
cw_solana/lib/solana_client.dart
+553 -334
@@ -4,54 +4,59 @@ import 'dart:math' as math;
4
5 import 'package:cw_core/crypto_currency.dart';
6 import 'package:cw_core/node.dart';
7 +import 'package:cw_core/solana_rpc_http_service.dart';
8 import 'package:cw_core/utils/print_verbose.dart';
9 import 'package:cw_solana/pending_solana_transaction.dart';
10 import 'package:cw_solana/solana_balance.dart';
11 import 'package:cw_solana/solana_exceptions.dart';
12 import 'package:cw_solana/solana_transaction_model.dart';
13 +import 'package:cw_solana/spl_token.dart';
14 import 'package:http/http.dart' as http;
13 -import 'package:solana/dto.dart';
14 -import 'package:solana/encoder.dart';
15 -import 'package:solana/solana.dart';
15 +import 'package:on_chain/solana/solana.dart';
16 +import 'package:on_chain/solana/src/models/pda/pda.dart';
17 +import 'package:blockchain_utils/blockchain_utils.dart';
18 import '.secrets.g.dart' as secrets;
19
20 class SolanaWalletClient {
21 final httpClient = http.Client();
20 - SolanaClient? _client;
22 + SolanaRPC? _provider;
23
24 bool connect(Node node) {
25 try {
24 - Uri rpcUri = node.uri;
25 - String webSocketUrl = 'wss://${node.uriRaw}';
26 + String formattedUrl;
27 + String protocolUsed = node.isSSL ? "https" : "http";
28
29 if (node.uriRaw == 'rpc.ankr.com') {
30 String ankrApiKey = secrets.ankrApiKey;
31
30 - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
31 - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
32 + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
33 } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
34 String chainStackApiKey = secrets.chainStackApiKey;
35
35 - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey');
36 - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey';
36 + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
37 + } else {
38 + formattedUrl = '$protocolUsed://${node.uriRaw}';
39 }
40
39 - _client = SolanaClient(
40 - rpcUrl: rpcUri,
41 - websocketUrl: Uri.parse(webSocketUrl),
42 - timeout: const Duration(minutes: 2),
43 - );
41 + _provider = SolanaRPC(SolanaRPCHTTPService(url: formattedUrl));
42 +
43 return true;
44 } catch (e) {
45 return false;
46 }
47 }
48
50 - Future<double> getBalance(String address) async {
49 + Future<double> getBalance(String walletAddress) async {
50 try {
52 - final balance = await _client!.rpcClient.getBalance(address);
51 + final balance = await _provider!.requestWithContext(
52 + SolanaRPCGetBalance(
53 + account: SolAddress(walletAddress),
54 + ),
55 + );
56 +
57 + final balInLamp = balance.result.toDouble();
58
54 - final solBalance = balance.value / lamportsPerSol;
59 + final solBalance = balInLamp / SolanaUtils.lamportsPerSol;
60
61 return solBalance;
62 } catch (_) {
@@ -59,37 +64,42 @@ class SolanaWalletClient {
64 }
65 }
66
62 - Future<ProgramAccountsResult?> getSPLTokenAccounts(String mintAddress, String publicKey) async {
67 + Future<List<TokenAccountResponse>?> getSPLTokenAccounts(
68 + String mintAddress, String publicKey) async {
69 try {
64 - final tokenAccounts = await _client!.rpcClient.getTokenAccountsByOwner(
65 - publicKey,
66 - TokenAccountsFilter.byMint(mintAddress),
67 - commitment: Commitment.confirmed,
68 - encoding: Encoding.jsonParsed,
70 + final result = await _provider!.request(
71 + SolanaRPCGetTokenAccountsByOwner(
72 + account: SolAddress(publicKey),
73 + mint: SolAddress(mintAddress),
74 + commitment: Commitment.confirmed,
75 + encoding: SolanaRPCEncoding.base64,
76 + ),
77 );
70 - return tokenAccounts;
78 +
79 + return result;
80 } catch (e) {
81 return null;
82 }
83 }
84
76 - Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String publicKey) async {
85 + Future<SolanaBalance?> getSplTokenBalance(String mintAddress, String walletAddress) async {
86 // Fetch the token accounts (a token can have multiple accounts for various uses)
78 - final tokenAccounts = await getSPLTokenAccounts(mintAddress, publicKey);
87 + final tokenAccounts = await getSPLTokenAccounts(mintAddress, walletAddress);
88
89 // Handle scenario where there is no token account
81 - if (tokenAccounts == null || tokenAccounts.value.isEmpty) {
90 + if (tokenAccounts == null || tokenAccounts.isEmpty) {
91 return null;
92 }
93
94 // Sum the balances of all accounts with the specified mint address
95 double totalBalance = 0.0;
96
88 - for (var programAccount in tokenAccounts.value) {
89 - final tokenAmountResult =
90 - await _client!.rpcClient.getTokenAccountBalance(programAccount.pubkey);
97 + for (var tokenAccount in tokenAccounts) {
98 + final tokenAmountResult = await _provider!.request(
99 + SolanaRPCGetTokenAccountBalance(account: tokenAccount.pubkey),
100 + );
101
92 - final balance = tokenAmountResult.value.uiAmountString;
102 + final balance = tokenAmountResult.uiAmountString;
103
104 final balanceAsDouble = double.tryParse(balance ?? '0.0') ?? 0.0;
105
@@ -101,198 +111,318 @@ class SolanaWalletClient {
111
112 Future<double> getFeeForMessage(String message, Commitment commitment) async {
113 try {
104 - final feeForMessage =
105 - await _client!.rpcClient.getFeeForMessage(message, commitment: commitment);
106 - final fee = (feeForMessage ?? 0.0) / lamportsPerSol;
114 + final feeForMessage = await _provider!.request(
115 + SolanaRPCGetFeeForMessage(
116 + encodedMessage: message,
117 + commitment: commitment,
118 + ),
119 + );
120 +
121 + final fee = (feeForMessage?.toDouble() ?? 0.0) / SolanaUtils.lamportsPerSol;
122 return fee;
123 } catch (_) {
124 return 0.0;
125 }
126 }
127
113 - Future<double> getEstimatedFee(Ed25519HDKeyPair ownerKeypair) async {
114 - const commitment = Commitment.confirmed;
115 -
116 - final message =
117 - _getMessageForNativeTransaction(ownerKeypair, ownerKeypair.address, lamportsPerSol);
118 -
119 - final latestBlockhash = await _getLatestBlockhash(commitment);
128 + Future<double> getEstimatedFee(SolanaPublicKey publicKey, Commitment commitment) async {
129 + final message = await _getMessageForNativeTransaction(
130 + publicKey: publicKey,
131 + destinationAddress: publicKey.toAddress().address,
132 + lamports: SolanaUtils.lamportsPerSol,
133 + commitment: commitment,
134 + );
135
121 - final estimatedFee = _getFeeFromCompiledMessage(
136 + final estimatedFee = await _getFeeFromCompiledMessage(
137 message,
123 - ownerKeypair.publicKey,
124 - latestBlockhash,
138 commitment,
139 );
140 return estimatedFee;
141 }
142
143 + Future<SolanaTransactionModel?> parseTransaction({
144 + VersionedTransactionResponse? txResponse,
145 + required String walletAddress,
146 + String? splTokenSymbol,
147 + }) async {
148 + if (txResponse == null) return null;
149 +
150 + try {
151 + final blockTime = txResponse.blockTime;
152 + final meta = txResponse.meta;
153 + final transaction = txResponse.transaction;
154 +
155 + if (meta == null || transaction == null) return null;
156 +
157 + final int fee = meta.fee;
158 +
159 + final message = transaction.message;
160 + final instructions = message.compiledInstructions;
161 +
162 + String sender = "";
163 + String receiver = "";
164 +
165 + String signature = (txResponse.transaction?.signatures.isEmpty ?? true)
166 + ? ""
167 + : Base58Encoder.encode(txResponse.transaction!.signatures.first);
168 +
169 + for (final instruction in instructions) {
170 + final programId = message.accountKeys[instruction.programIdIndex];
171 +
172 + if (programId == SystemProgramConst.programId) {
173 + // For native solana transactions
174 + if (instruction.accounts.length < 2) continue;
175 + final senderIndex = instruction.accounts[0];
176 + final receiverIndex = instruction.accounts[1];
177 +
178 + sender = message.accountKeys[senderIndex].address;
179 + receiver = message.accountKeys[receiverIndex].address;
180 +
181 + final feeForTx = fee / SolanaUtils.lamportsPerSol;
182 +
183 + final preBalances = meta.preBalances;
184 + final postBalances = meta.postBalances;
185 +
186 + final amountInString =
187 + (((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9))
188 + .toDouble() -
189 + feeForTx)
190 + .toStringAsFixed(6);
191 +
192 + final amount = double.parse(amountInString);
193 +
194 + return SolanaTransactionModel(
195 + isOutgoingTx: sender == walletAddress,
196 + from: sender,
197 + to: receiver,
198 + id: signature,
199 + amount: amount.abs(),
200 + programId: SystemProgramConst.programId.address,
201 + tokenSymbol: 'SOL',
202 + blockTimeInInt: blockTime?.toInt() ?? 0,
203 + fee: feeForTx,
204 + );
205 + } else if (programId == SPLTokenProgramConst.tokenProgramId) {
206 + // For SPL Token transactions
207 + if (instruction.accounts.length < 2) continue;
208 +
209 + final preBalances = meta.preTokenBalances;
210 + final postBalances = meta.postTokenBalances;
211 +
212 + double amount = 0.0;
213 + bool isOutgoing = false;
214 + String? mintAddress;
215 +
216 + double userPreAmount = 0.0;
217 + if (preBalances != null && preBalances.isNotEmpty) {
218 + for (final preBal in preBalances) {
219 + if (preBal.owner?.address == walletAddress) {
220 + userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
221 +
222 + mintAddress = preBal.mint.address;
223 + break;
224 + }
225 + }
226 + }
227 +
228 + double userPostAmount = 0.0;
229 + if (postBalances != null && postBalances.isNotEmpty) {
230 + for (final postBal in postBalances) {
231 + if (postBal.owner?.address == walletAddress) {
232 + userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
233 +
234 + mintAddress ??= postBal.mint.address;
235 + break;
236 + }
237 + }
238 + }
239 +
240 + final diff = userPreAmount - userPostAmount;
241 + final rawAmount = diff.abs();
242 +
243 + final amountInString = rawAmount.toStringAsFixed(6);
244 + amount = double.parse(amountInString);
245 +
246 + isOutgoing = diff > 0;
247 +
248 + if (mintAddress == null && instruction.accounts.length >= 4) {
249 + final mintIndex = instruction.accounts[3];
250 + mintAddress = message.accountKeys[mintIndex].address;
251 + }
252 +
253 + final sender = message.accountKeys[instruction.accounts[0]].address;
254 + final receiver = message.accountKeys[instruction.accounts[1]].address;
255 +
256 + String? tokenSymbol = splTokenSymbol;
257 + if (tokenSymbol == null && mintAddress != null) {
258 + final token = await fetchSPLTokenInfo(mintAddress);
259 + tokenSymbol = token?.symbol;
260 + }
261 +
262 + return SolanaTransactionModel(
263 + isOutgoingTx: isOutgoing,
264 + from: sender,
265 + to: receiver,
266 + id: signature,
267 + amount: amount,
268 + programId: SPLTokenProgramConst.tokenProgramId.address,
269 + blockTimeInInt: blockTime?.toInt() ?? 0,
270 + tokenSymbol: tokenSymbol ?? '',
271 + fee: fee / SolanaUtils.lamportsPerSol,
272 + );
273 + } else {
274 + return null;
275 + }
276 + }
277 + } catch (e, s) {
278 + printV("Error parsing transaction: $e\n$s");
279 + }
280 +
281 + return null;
282 + }
283 +
284 /// Load the Address's transactions into the account
285 Future<List<SolanaTransactionModel>> fetchTransactions(
132 - Ed25519HDPublicKey publicKey, {
286 + SolAddress address, {
287 String? splTokenSymbol,
288 int? splTokenDecimal,
289 + Commitment? commitment,
290 + SolAddress? walletAddress,
291 }) async {
292 List<SolanaTransactionModel> transactions = [];
293
294 try {
139 - final signatures = await _client!.rpcClient.getSignaturesForAddress(
140 - publicKey.toBase58(),
141 - commitment: Commitment.confirmed,
295 + final signatures = await _provider!.request(
296 + SolanaRPCGetSignaturesForAddress(
297 + account: address,
298 + commitment: commitment,
299 + ),
300 );
301
144 - final List<TransactionDetails> transactionDetails = [];
302 + final List<VersionedTransactionResponse?> transactionDetails = [];
303 +
304 for (int i = 0; i < signatures.length; i += 20) {
146 - final response = await _client!.rpcClient.getMultipleTransactions(
147 - signatures.sublist(i, math.min(i + 20, signatures.length)),
148 - commitment: Commitment.confirmed,
149 - encoding: Encoding.jsonParsed,
150 - );
151 - transactionDetails.addAll(response);
305 + final batch = signatures.skip(i).take(20).toList(); // Get the next 20 signatures
306 +
307 + final batchResponses = await Future.wait(batch.map((signature) async {
308 + try {
309 + return await _provider!.request(
310 + SolanaRPCGetTransaction(
311 + transactionSignature: signature['signature'],
312 + encoding: SolanaRPCEncoding.jsonParsed,
313 + maxSupportedTransactionVersion: 0,
314 + ),
315 + );
316 + } catch (e) {
317 + printV("Error fetching transaction: $e");
318 + return null;
319 + }
320 + }));
321 +
322 + transactionDetails.addAll(batchResponses.whereType<VersionedTransactionResponse>());
323
324 // to avoid reaching the node RPS limit
154 - await Future.delayed(Duration(milliseconds: 500));
325 + if (i + 20 < signatures.length) {
326 + await Future.delayed(const Duration(milliseconds: 500));
327 + }
328 }
329
330 for (final tx in transactionDetails) {
158 - if (tx.transaction is ParsedTransaction) {
159 - final parsedTx = (tx.transaction as ParsedTransaction);
160 - final message = parsedTx.message;
161 -
162 - final fee = (tx.meta?.fee ?? 0) / lamportsPerSol;
163 -
164 - for (final instruction in message.instructions) {
165 - if (instruction is ParsedInstruction) {
166 - instruction.map(
167 - system: (systemData) {
168 - systemData.parsed.map(
169 - transfer: (transferData) {
170 - ParsedSystemTransferInformation transfer = transferData.info;
171 - bool isOutgoingTx = transfer.source == publicKey.toBase58();
172 -
173 - double amount = transfer.lamports.toDouble() / lamportsPerSol;
174 -
175 - transactions.add(
176 - SolanaTransactionModel(
177 - id: parsedTx.signatures.first,
178 - from: transfer.source,
179 - to: transfer.destination,
180 - amount: amount,
181 - isOutgoingTx: isOutgoingTx,
182 - blockTimeInInt: tx.blockTime!,
183 - fee: fee,
184 - programId: SystemProgram.programId,
185 - tokenSymbol: 'SOL',
186 - ),
187 - );
188 - },
189 - transferChecked: (_) {},
190 - unsupported: (_) {},
191 - );
192 - },
193 - splToken: (splTokenData) {
194 - if (splTokenSymbol != null) {
195 - splTokenData.parsed.map(
196 - transfer: (transferData) {
197 - SplTokenTransferInfo transfer = transferData.info;
198 - bool isOutgoingTx = transfer.source == publicKey.toBase58();
199 -
200 - double amount = (double.tryParse(transfer.amount) ?? 0.0) /
201 - math.pow(10, splTokenDecimal ?? 9);
202 -
203 - transactions.add(
204 - SolanaTransactionModel(
205 - id: parsedTx.signatures.first,
206 - fee: fee,
207 - from: transfer.source,
208 - to: transfer.destination,
209 - amount: amount,
210 - isOutgoingTx: isOutgoingTx,
211 - programId: TokenProgram.programId,
212 - blockTimeInInt: tx.blockTime!,
213 - tokenSymbol: splTokenSymbol,
214 - ),
215 - );
216 - },
217 - transferChecked: (transferCheckedData) {
218 - SplTokenTransferCheckedInfo transfer = transferCheckedData.info;
219 - bool isOutgoingTx = transfer.source == publicKey.toBase58();
220 - double amount =
221 - double.tryParse(transfer.tokenAmount.uiAmountString ?? '0.0') ?? 0.0;
222 -
223 - transactions.add(
224 - SolanaTransactionModel(
225 - id: parsedTx.signatures.first,
226 - fee: fee,
227 - from: transfer.source,
228 - to: transfer.destination,
229 - amount: amount,
230 - isOutgoingTx: isOutgoingTx,
231 - programId: TokenProgram.programId,
232 - blockTimeInInt: tx.blockTime!,
233 - tokenSymbol: splTokenSymbol,
234 - ),
235 - );
236 - },
237 - generic: (genericData) {},
238 - );
239 - }
240 - },
241 - memo: (_) {},
242 - unsupported: (a) {},
243 - );
244 - }
245 - }
331 + final parsedTx = await parseTransaction(
332 + txResponse: tx,
333 + splTokenSymbol: splTokenSymbol,
334 + walletAddress: walletAddress?.address ?? address.address,
335 + );
336 + if (parsedTx != null) {
337 + transactions.add(parsedTx);
338 }
339 }
340
341 return transactions;
250 - } catch (err) {
342 + } catch (err, s) {
343 + printV('Error fetching transactions: $err \n$s');
344 return [];
345 }
346 }
347
255 - Future<List<SolanaTransactionModel>> getSPLTokenTransfers(
256 - String address,
257 - String splTokenSymbol,
258 - int splTokenDecimal,
259 - Ed25519HDKeyPair ownerKeypair,
260 - ) async {
261 - final tokenMint = Ed25519HDPublicKey.fromBase58(address);
262 -
263 - ProgramAccount? associatedTokenAccount;
264 -
348 + Future<List<SolanaTransactionModel>> getSPLTokenTransfers({
349 + required String mintAddress,
350 + required String splTokenSymbol,
351 + required int splTokenDecimal,
352 + required SolanaPrivateKey privateKey,
353 + }) async {
354 + ProgramDerivedAddress? associatedTokenAccount;
355 + final ownerWalletAddress = privateKey.publicKey().toAddress();
356 try {
266 - associatedTokenAccount = await _client!.getAssociatedTokenAccount(
267 - mint: tokenMint,
268 - owner: ownerKeypair.publicKey,
269 - commitment: Commitment.confirmed,
357 + associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
358 + payerPrivateKey: privateKey,
359 + mintAddress: SolAddress(mintAddress),
360 + ownerAddress: ownerWalletAddress,
361 + shouldCreateATA: false,
362 );
271 - } catch (_) {}
363 + } catch (e, s) {
364 + printV('$e \n $s');
365 + }
366
367 if (associatedTokenAccount == null) return [];
368
275 - final accountPublicKey = Ed25519HDPublicKey.fromBase58(associatedTokenAccount.pubkey);
369 + final accountPublicKey = associatedTokenAccount.address;
370
371 final tokenTransactions = await fetchTransactions(
372 accountPublicKey,
373 splTokenSymbol: splTokenSymbol,
374 splTokenDecimal: splTokenDecimal,
375 + walletAddress: ownerWalletAddress,
376 );
377
378 return tokenTransactions;
379 }
380
381 + Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
382 + final programAddress =
383 + MetaplexTokenMetaDataProgramUtils.findMetadataPda(mint: SolAddress(mintAddress));
384 +
385 + final token = await _provider!.request(
386 + SolanaRPCGetMetadataAccount(
387 + account: programAddress.address,
388 + commitment: Commitment.confirmed,
389 + ),
390 + );
391 +
392 + if (token == null) {
393 + return null;
394 + }
395 +
396 + final metadata = token.data;
397 +
398 + String? iconPath;
399 + //TODO(Further explore fetching images)
400 + // try {
401 + // iconPath = await _client.getIconImageFromTokenUri(metadata.uri);
402 + // } catch (_) {}
403 +
404 + String filteredTokenSymbol =
405 + metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
406 +
407 + return SPLToken.fromMetadata(
408 + name: metadata.name,
409 + mint: metadata.symbol,
410 + symbol: filteredTokenSymbol,
411 + mintAddress: token.mint.address,
412 + iconPath: iconPath,
413 + );
414 + }
415 +
416 void stop() {}
417
288 - SolanaClient? get getSolanaClient => _client;
418 + SolanaRPC? get getSolanaProvider => _provider;
419
420 Future<PendingSolanaTransaction> signSolanaTransaction({
421 required String tokenTitle,
422 required int tokenDecimals,
423 required double inputAmount,
424 required String destinationAddress,
295 - required Ed25519HDKeyPair ownerKeypair,
425 + required SolanaPrivateKey ownerPrivateKey,
426 required bool isSendAll,
427 required double solBalance,
428 String? tokenMint,
@@ -302,11 +432,9 @@ class SolanaWalletClient {
432
433 if (tokenTitle == CryptoCurrency.sol.title) {
434 final pendingNativeTokenTransaction = await _signNativeTokenTransaction(
305 - tokenTitle: tokenTitle,
306 - tokenDecimals: tokenDecimals,
435 inputAmount: inputAmount,
436 destinationAddress: destinationAddress,
309 - ownerKeypair: ownerKeypair,
437 + ownerPrivateKey: ownerPrivateKey,
438 commitment: commitment,
439 isSendAll: isSendAll,
440 solBalance: solBalance,
@@ -314,12 +442,11 @@ class SolanaWalletClient {
442 return pendingNativeTokenTransaction;
443 } else {
444 final pendingSPLTokenTransaction = _signSPLTokenTransaction(
317 - tokenTitle: tokenTitle,
445 tokenDecimals: tokenDecimals,
446 tokenMint: tokenMint!,
447 inputAmount: inputAmount,
448 + ownerPrivateKey: ownerPrivateKey,
449 destinationAddress: destinationAddress,
322 - ownerKeypair: ownerKeypair,
450 commitment: commitment,
451 solBalance: solBalance,
452 );
@@ -327,47 +454,72 @@ class SolanaWalletClient {
454 }
455 }
456
330 - Future<LatestBlockhash> _getLatestBlockhash(Commitment commitment) async {
331 - final latestBlockHashResult =
332 - await _client!.rpcClient.getLatestBlockhash(commitment: commitment).value;
333 -
334 - final latestBlockhash = LatestBlockhash(
335 - blockhash: latestBlockHashResult.blockhash,
336 - lastValidBlockHeight: latestBlockHashResult.lastValidBlockHeight,
457 + Future<SolAddress> _getLatestBlockhash(Commitment commitment) async {
458 + final latestBlockhash = await _provider!.request(
459 + const SolanaRPCGetLatestBlockhash(),
460 );
461
339 - return latestBlockhash;
462 + return latestBlockhash.blockhash;
463 }
464
342 - Message _getMessageForNativeTransaction(
343 - Ed25519HDKeyPair ownerKeypair,
344 - String destinationAddress,
345 - int lamports,
346 - ) {
465 + Future<Message> _getMessageForNativeTransaction({
466 + required SolanaPublicKey publicKey,
467 + required String destinationAddress,
468 + required int lamports,
469 + required Commitment commitment,
470 + }) async {
471 final instructions = [
348 - SystemInstruction.transfer(
349 - fundingAccount: ownerKeypair.publicKey,
350 - recipientAccount: Ed25519HDPublicKey.fromBase58(destinationAddress),
351 - lamports: lamports,
472 + SystemProgram.transfer(
473 + from: publicKey.toAddress(),
474 + layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
475 + to: SolAddress(destinationAddress),
476 ),
477 ];
478
355 - final message = Message(instructions: instructions);
479 + final latestBlockhash = await _getLatestBlockhash(commitment);
480 +
481 + final message = Message.compile(
482 + transactionInstructions: instructions,
483 + payer: publicKey.toAddress(),
484 + recentBlockhash: latestBlockhash,
485 + );
486 return message;
487 }
488
359 - Future<double> _getFeeFromCompiledMessage(
360 - Message message,
361 - Ed25519HDPublicKey feePayer,
362 - LatestBlockhash latestBlockhash,
363 - Commitment commitment,
364 - ) async {
365 - final compile = message.compile(
366 - recentBlockhash: latestBlockhash.blockhash,
367 - feePayer: feePayer,
489 + Future<Message> _getMessageForSPLTokenTransaction({
490 + required SolAddress ownerAddress,
491 + required SolAddress destinationAddress,
492 + required int tokenDecimals,
493 + required SolAddress mintAddress,
494 + required SolAddress sourceAccount,
495 + required int amount,
496 + required Commitment commitment,
497 + }) async {
498 + final instructions = [
499 + SPLTokenProgram.transferChecked(
500 + layout: SPLTokenTransferCheckedLayout(
501 + amount: BigInt.from(amount),
502 + decimals: tokenDecimals,
503 + ),
504 + mint: mintAddress,
505 + source: sourceAccount,
506 + destination: destinationAddress,
507 + owner: ownerAddress,
508 + )
509 + ];
510 +
511 + final latestBlockhash = await _getLatestBlockhash(commitment);
512 +
513 + final message = Message.compile(
514 + transactionInstructions: instructions,
515 + payer: ownerAddress,
516 + recentBlockhash: latestBlockhash,
517 );
518 + return message;
519 + }
520
370 - final base64Message = base64Encode(compile.toByteArray().toList());
521 + Future<double> _getFeeFromCompiledMessage(Message message, Commitment commitment) async {
522 + final base64Message = base64Encode(message.serialize());
523
524 final fee = await getFeeForMessage(base64Message, commitment);
525
@@ -379,43 +531,43 @@ class SolanaWalletClient {
531 required double solBalance,
532 required double fee,
533 }) async {
382 - return true;
383 - // TODO: this is not doing what the name inclines
384 - // final rent =
385 - // await _client!.getMinimumBalanceForMintRentExemption(commitment: Commitment.confirmed);
386 - //
387 - // final rentInSol = (rent / lamportsPerSol).toDouble();
388 - //
389 - // final remnant = solBalance - (inputAmount + fee);
390 - //
391 - // if (remnant > rentInSol) return true;
392 - //
393 - // return false;
534 + final rent = await _provider!.request(
535 + SolanaRPCGetMinimumBalanceForRentExemption(
536 + size: SolanaTokenAccountUtils.accountSize,
537 + ),
538 + );
539 +
540 + final rentInSol = (rent.toDouble() / SolanaUtils.lamportsPerSol).toDouble();
541 +
542 + final remnant = solBalance - (inputAmount + fee);
543 +
544 + if (remnant > rentInSol) return true;
545 +
546 + return false;
547 }
548
549 Future<PendingSolanaTransaction> _signNativeTokenTransaction({
397 - required String tokenTitle,
398 - required int tokenDecimals,
550 required double inputAmount,
551 required String destinationAddress,
401 - required Ed25519HDKeyPair ownerKeypair,
552 + required SolanaPrivateKey ownerPrivateKey,
553 required Commitment commitment,
554 required bool isSendAll,
555 required double solBalance,
556 }) async {
557 // Convert SOL to lamport
407 - int lamports = (inputAmount * lamportsPerSol).toInt();
408 -
409 - Message message = _getMessageForNativeTransaction(ownerKeypair, destinationAddress, lamports);
558 + int lamports = (inputAmount * SolanaUtils.lamportsPerSol).toInt();
559
411 - final signers = [ownerKeypair];
560 + Message message = await _getMessageForNativeTransaction(
561 + publicKey: ownerPrivateKey.publicKey(),
562 + destinationAddress: destinationAddress,
563 + lamports: lamports,
564 + commitment: commitment,
565 + );
566
413 - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment);
567 + SolAddress latestBlockhash = await _getLatestBlockhash(commitment);
568
569 final fee = await _getFeeFromCompiledMessage(
570 message,
417 - signers.first.publicKey,
418 - latestBlockhash,
571 commitment,
572 );
573
@@ -429,37 +581,44 @@ class SolanaWalletClient {
581 throw SolanaSignNativeTokenTransactionRentException();
582 }
583
432 - SignedTx signedTx;
584 + String serializedTransaction;
585 if (isSendAll) {
434 - final feeInLamports = (fee * lamportsPerSol).toInt();
586 + final feeInLamports = (fee * SolanaUtils.lamportsPerSol).toInt();
587 final updatedLamports = lamports - feeInLamports;
588
437 - final updatedMessage =
438 - _getMessageForNativeTransaction(ownerKeypair, destinationAddress, updatedLamports);
439 -
440 - signedTx = await _signTransactionInternal(
441 - message: updatedMessage,
442 - signers: signers,
443 - commitment: commitment,
589 + final transaction = _constructNativeTransaction(
590 + ownerPrivateKey: ownerPrivateKey,
591 + destinationAddress: destinationAddress,
592 latestBlockhash: latestBlockhash,
593 + lamports: updatedLamports,
594 + );
595 +
596 + serializedTransaction = await _signTransactionInternal(
597 + ownerPrivateKey: ownerPrivateKey,
598 + transaction: transaction,
599 );
600 } else {
447 - signedTx = await _signTransactionInternal(
448 - message: message,
449 - signers: signers,
450 - commitment: commitment,
601 + final transaction = _constructNativeTransaction(
602 + ownerPrivateKey: ownerPrivateKey,
603 + destinationAddress: destinationAddress,
604 latestBlockhash: latestBlockhash,
605 + lamports: lamports,
606 + );
607 +
608 + serializedTransaction = await _signTransactionInternal(
609 + ownerPrivateKey: ownerPrivateKey,
610 + transaction: transaction,
611 );
612 }
613
614 sendTx() async => await sendTransaction(
456 - signedTransaction: signedTx,
615 + serializedTransaction: serializedTransaction,
616 commitment: commitment,
617 );
618
619 final pendingTransaction = PendingSolanaTransaction(
620 amount: inputAmount,
462 - signedTransaction: signedTx,
621 + serializedTransaction: serializedTransaction,
622 destinationAddress: destinationAddress,
623 sendTransaction: sendTx,
624 fee: fee,
@@ -468,108 +627,170 @@ class SolanaWalletClient {
627 return pendingTransaction;
628 }
629
630 + SolanaTransaction _constructNativeTransaction({
631 + required SolanaPrivateKey ownerPrivateKey,
632 + required String destinationAddress,
633 + required SolAddress latestBlockhash,
634 + required int lamports,
635 + }) {
636 + final owner = ownerPrivateKey.publicKey().toAddress();
637 +
638 + /// Create a transfer instruction to move funds from the owner to the receiver.
639 + final transferInstruction = SystemProgram.transfer(
640 + from: owner,
641 + layout: SystemTransferLayout(lamports: BigInt.from(lamports)),
642 + to: SolAddress(destinationAddress),
643 + );
644 +
645 + /// Construct a Solana transaction with the transfer instruction.
646 + return SolanaTransaction(
647 + instructions: [transferInstruction],
648 + recentBlockhash: latestBlockhash,
649 + payerKey: ownerPrivateKey.publicKey().toAddress(),
650 + type: TransactionType.v0,
651 + );
652 + }
653 +
654 + Future<ProgramDerivedAddress?> _getOrCreateAssociatedTokenAccount({
655 + required SolanaPrivateKey payerPrivateKey,
656 + required SolAddress ownerAddress,
657 + required SolAddress mintAddress,
658 + required bool shouldCreateATA,
659 + }) async {
660 + final associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
661 + mint: mintAddress,
662 + owner: ownerAddress,
663 + );
664 +
665 + SolanaAccountInfo? accountInfo;
666 + try {
667 + accountInfo = await _provider!.request(
668 + SolanaRPCGetAccountInfo(account: associatedTokenAccount.address),
669 + );
670 + } catch (e) {
671 + accountInfo = null;
672 + }
673 +
674 + // If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain.
675 + if (accountInfo != null) return associatedTokenAccount;
676 +
677 + if (!shouldCreateATA) return null;
678 +
679 + final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount(
680 + payer: payerPrivateKey.publicKey().toAddress(),
681 + associatedToken: associatedTokenAccount.address,
682 + owner: ownerAddress,
683 + mint: mintAddress,
684 + );
685 +
686 + final blockhash = await _getLatestBlockhash(Commitment.confirmed);
687 +
688 + final transaction = SolanaTransaction(
689 + payerKey: payerPrivateKey.publicKey().toAddress(),
690 + instructions: [createAssociatedTokenAccount],
691 + recentBlockhash: blockhash,
692 + );
693 +
694 + transaction.sign([payerPrivateKey]);
695 +
696 + await sendTransaction(
697 + serializedTransaction: transaction.serializeString(),
698 + commitment: Commitment.confirmed,
699 + );
700 +
701 + // Delay for propagation on the blockchain for newly created associated token addresses
702 + await Future.delayed(const Duration(seconds: 2));
703 +
704 + return associatedTokenAccount;
705 + }
706 +
707 Future<PendingSolanaTransaction> _signSPLTokenTransaction({
472 - required String tokenTitle,
708 required int tokenDecimals,
709 required String tokenMint,
710 required double inputAmount,
711 required String destinationAddress,
477 - required Ed25519HDKeyPair ownerKeypair,
712 + required SolanaPrivateKey ownerPrivateKey,
713 required Commitment commitment,
714 required double solBalance,
715 }) async {
481 - final destinationOwner = Ed25519HDPublicKey.fromBase58(destinationAddress);
482 - final mint = Ed25519HDPublicKey.fromBase58(tokenMint);
716 + final mintAddress = SolAddress(tokenMint);
717
718 // Input by the user
719 final amount = (inputAmount * math.pow(10, tokenDecimals)).toInt();
486 -
487 - ProgramAccount? associatedRecipientAccount;
488 - ProgramAccount? associatedSenderAccount;
489 -
490 - associatedRecipientAccount = await _client!.getAssociatedTokenAccount(
491 - mint: mint,
492 - owner: destinationOwner,
493 - commitment: commitment,
494 - );
495 -
496 - associatedSenderAccount = await _client!.getAssociatedTokenAccount(
497 - owner: ownerKeypair.publicKey,
498 - mint: mint,
499 - commitment: commitment,
500 - );
720 + ProgramDerivedAddress? associatedSenderAccount;
721 + try {
722 + associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
723 + mint: mintAddress,
724 + owner: ownerPrivateKey.publicKey().toAddress(),
725 + );
726 + } catch (e) {
727 + associatedSenderAccount = null;
728 + }
729
730 // Throw an appropriate exception if the sender has no associated
731 // token account
732 if (associatedSenderAccount == null) {
505 - throw SolanaNoAssociatedTokenAccountException(ownerKeypair.address, mint.toBase58());
733 + throw SolanaNoAssociatedTokenAccountException(
734 + ownerPrivateKey.publicKey().toAddress().address,
735 + mintAddress.address,
736 + );
737 }
738
739 + ProgramDerivedAddress? associatedRecipientAccount;
740 try {
509 - if (associatedRecipientAccount == null) {
510 - final derivedAddress = await findAssociatedTokenAddress(
511 - owner: destinationOwner,
512 - mint: mint,
513 - );
514 -
515 - final instruction = AssociatedTokenAccountInstruction.createAccount(
516 - mint: mint,
517 - address: derivedAddress,
518 - owner: destinationOwner,
519 - funder: ownerKeypair.publicKey,
520 - );
521 -
522 - final _signedTx = await _signTransactionInternal(
523 - message: Message.only(instruction),
524 - signers: [ownerKeypair],
525 - commitment: commitment,
526 - latestBlockhash: await _getLatestBlockhash(commitment),
527 - );
528 -
529 - await sendTransaction(
530 - signedTransaction: _signedTx,
531 - commitment: commitment,
532 - );
741 + associatedRecipientAccount = await _getOrCreateAssociatedTokenAccount(
742 + payerPrivateKey: ownerPrivateKey,
743 + mintAddress: mintAddress,
744 + ownerAddress: SolAddress(destinationAddress),
745 + shouldCreateATA: true,
746 + );
747 + } catch (e) {
748 + associatedRecipientAccount = null;
749
534 - associatedRecipientAccount = ProgramAccount(
535 - pubkey: derivedAddress.toBase58(),
536 - account: Account(
537 - owner: destinationOwner.toBase58(),
538 - lamports: 0,
539 - executable: false,
540 - rentEpoch: BigInt.zero,
541 - data: null,
542 - ),
543 - );
750 + throw SolanaCreateAssociatedTokenAccountException(
751 + 'Error fetching recipient associated token account: ${e.toString()}',
752 + );
753 + }
754
545 - await Future.delayed(Duration(seconds: 5));
546 - }
547 - } catch (e) {
548 - throw SolanaCreateAssociatedTokenAccountException(e.toString());
755 + if (associatedRecipientAccount == null) {
756 + throw SolanaCreateAssociatedTokenAccountException(
757 + 'Error fetching recipient associated token account',
758 + );
759 }
760
551 - final instruction = TokenInstruction.transfer(
552 - source: Ed25519HDPublicKey.fromBase58(associatedSenderAccount.pubkey),
553 - destination: Ed25519HDPublicKey.fromBase58(associatedRecipientAccount.pubkey),
554 - owner: ownerKeypair.publicKey,
555 - amount: amount,
761 + final transferInstructions = SPLTokenProgram.transferChecked(
762 + layout: SPLTokenTransferCheckedLayout(
763 + amount: BigInt.from(amount),
764 + decimals: tokenDecimals,
765 + ),
766 + mint: mintAddress,
767 + source: associatedSenderAccount.address,
768 + destination: associatedRecipientAccount.address,
769 + owner: ownerPrivateKey.publicKey().toAddress(),
770 );
771
558 - final message = Message(instructions: [instruction]);
559 -
560 - final signers = [ownerKeypair];
772 + final latestBlockHash = await _getLatestBlockhash(commitment);
773
562 - LatestBlockhash latestBlockhash = await _getLatestBlockhash(commitment);
774 + final transaction = SolanaTransaction(
775 + payerKey: ownerPrivateKey.publicKey().toAddress(),
776 + instructions: [transferInstructions],
777 + recentBlockhash: latestBlockHash,
778 + );
779
564 - final fee = await _getFeeFromCompiledMessage(
565 - message,
566 - signers.first.publicKey,
567 - latestBlockhash,
568 - commitment,
780 + final message = await _getMessageForSPLTokenTransaction(
781 + ownerAddress: ownerPrivateKey.publicKey().toAddress(),
782 + tokenDecimals: tokenDecimals,
783 + mintAddress: mintAddress,
784 + destinationAddress: associatedRecipientAccount.address,
785 + sourceAccount: associatedSenderAccount.address,
786 + amount: amount,
787 + commitment: commitment,
788 );
789
790 + final fee = await _getFeeFromCompiledMessage(message, commitment);
791 +
792 bool hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
572 - inputAmount: inputAmount,
793 + inputAmount: 0,
794 fee: fee,
795 solBalance: solBalance,
796 );
@@ -578,25 +799,19 @@ class SolanaWalletClient {
799 throw SolanaSignSPLTokenTransactionRentException();
800 }
801
581 - final signedTx = await _signTransactionInternal(
582 - message: message,
583 - signers: signers,
584 - commitment: commitment,
585 - latestBlockhash: latestBlockhash,
802 + final serializedTransaction = await _signTransactionInternal(
803 + ownerPrivateKey: ownerPrivateKey,
804 + transaction: transaction,
805 );
806
588 - sendTx() async {
589 - await Future.delayed(Duration(seconds: 3));
590 -
591 - return await sendTransaction(
592 - signedTransaction: signedTx,
807 + sendTx() async => await sendTransaction(
808 + serializedTransaction: serializedTransaction,
809 commitment: commitment,
810 );
595 - }
811
812 final pendingTransaction = PendingSolanaTransaction(
813 amount: inputAmount,
599 - signedTransaction: signedTx,
814 + serializedTransaction: serializedTransaction,
815 destinationAddress: destinationAddress,
816 sendTransaction: sendTx,
817 fee: fee,
@@ -604,37 +819,41 @@ class SolanaWalletClient {
819 return pendingTransaction;
820 }
821
607 - Future<SignedTx> _signTransactionInternal({
608 - required Message message,
609 - required List<Ed25519HDKeyPair> signers,
610 - required Commitment commitment,
611 - required LatestBlockhash latestBlockhash,
822 + Future<String> _signTransactionInternal({
823 + required SolanaPrivateKey ownerPrivateKey,
824 + required SolanaTransaction transaction,
825 }) async {
613 - final signedTx = await signTransaction(latestBlockhash, message, signers);
826 + /// Sign the transaction with the owner's private key.
827 + final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
828 + transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
829 +
830 + /// Serialize the transaction.
831 + final serializedTransaction = transaction.serializeString();
832
615 - return signedTx;
833 + return serializedTransaction;
834 }
835
836 Future<String> sendTransaction({
619 - required SignedTx signedTransaction,
837 + required String serializedTransaction,
838 required Commitment commitment,
839 }) async {
840 try {
623 - final signature = await _client!.rpcClient.sendTransaction(
624 - signedTransaction.encode(),
625 - preflightCommitment: commitment,
841 + /// Send the transaction to the Solana network.
842 + final signature = await _provider!.request(
843 + SolanaRPCSendTransaction(
844 + encodedTransaction: serializedTransaction,
845 + commitment: commitment,
846 + ),
847 );
627 -
628 - _client!.waitForSignatureStatus(signature, status: commitment);
629 -
848 return signature;
849 } catch (e) {
632 - printV('Error while sending transaction: ${e.toString()}');
850 throw Exception(e);
851 }
852 }
853
854 Future<String?> getIconImageFromTokenUri(String uri) async {
855 + if (uri.isEmpty || uri == '…') return null;
856 +
857 try {
858 final response = await httpClient.get(Uri.parse(uri));
859
cw_solana/lib/solana_transaction_info.dart
+3 -1
@@ -34,7 +34,9 @@ class SolanaTransactionInfo extends TransactionInfo {
34 @override
35 String amountFormatted() {
36 String stringBalance = solAmount.toString();
37 -
37 + if (stringBalance.toString().length >= 12) {
38 + stringBalance = stringBalance.substring(0, 12);
39 + }
40 return '$stringBalance $tokenSymbol';
41 }
42
cw_solana/lib/solana_wallet.dart
+63 -93
@@ -30,9 +30,9 @@ import 'package:hex/hex.dart';
30 import 'package:hive/hive.dart';
31 import 'package:mobx/mobx.dart';
32 import 'package:shared_preferences/shared_preferences.dart';
33 -import 'package:solana/base58.dart';
34 -import 'package:solana/metaplex.dart' as metaplex;
35 -import 'package:solana/solana.dart';
33 +import 'package:on_chain/solana/solana.dart' hide Store;
34 +import 'package:bip39/bip39.dart' as bip39;
35 +import 'package:blockchain_utils/blockchain_utils.dart';
36
37 part 'solana_wallet.g.dart';
38
@@ -77,14 +77,6 @@ abstract class SolanaWalletBase
77 final String? _hexPrivateKey;
78 final EncryptionFileUtils encryptionFileUtils;
79
80 - // The Solana WalletPair
81 - Ed25519HDKeyPair? _walletKeyPair;
82 -
83 - Ed25519HDKeyPair? get walletKeyPair => _walletKeyPair;
84 -
85 - // To access the privateKey bytes.
86 - Ed25519HDKeyPairData? _keyPairData;
87 -
80 late final SolanaWalletClient _client;
81
82 @observable
@@ -108,29 +100,23 @@ abstract class SolanaWalletBase
100 final Completer<SharedPreferences> _sharedPrefs = Completer();
101
102 @override
111 - Ed25519HDKeyPairData get keys {
112 - if (_keyPairData == null) {
113 - return Ed25519HDKeyPairData([], publicKey: const Ed25519HDPublicKey([]));
114 - }
103 + Object get keys => throw UnimplementedError("keys");
104
116 - return _keyPairData!;
117 - }
105 + late final SolanaPrivateKey _solanaPrivateKey;
106
119 - @override
120 - String? get seed => _mnemonic;
107 + late final SolanaPublicKey _solanaPublicKey;
108
122 - @override
123 - String get privateKey {
124 - final privateKeyBytes = _keyPairData!.bytes;
109 + SolanaPublicKey get solanaPublicKey => _solanaPublicKey;
110
126 - final publicKeyBytes = _keyPairData!.publicKey.bytes;
111 + SolanaPrivateKey get solanaPrivateKey => _solanaPrivateKey;
112
128 - final encodedBytes = privateKeyBytes + publicKeyBytes;
113 + String get solanaAddress => _solanaPublicKey.toAddress().address;
114
130 - final privateKey = base58encode(encodedBytes);
115 + @override
116 + String? get seed => _mnemonic;
117
132 - return privateKey;
133 - }
118 + @override
119 + String get privateKey => _solanaPrivateKey.seedHex();
120
121 @override
122 WalletKeysData get walletKeysData => WalletKeysData(mnemonic: _mnemonic, privateKey: privateKey);
@@ -140,35 +126,47 @@ abstract class SolanaWalletBase
126
127 splTokensBox = await CakeHive.openBox<SPLToken>(boxName);
128
143 - // Create WalletPair using either the mnemonic or the privateKey
144 - _walletKeyPair = await getWalletPair(
129 + // Create the privatekey using either the mnemonic or the privateKey
130 + _solanaPrivateKey = await getPrivateKey(
131 mnemonic: _mnemonic,
132 privateKey: _hexPrivateKey,
133 + passphrase: passphrase,
134 );
135
149 - // Extract the keyPairData containing both the privateKey bytes and the publicKey hex.
150 - _keyPairData = await _walletKeyPair!.extract();
136 + // Extract the public key and wallet address
137 + _solanaPublicKey = _solanaPrivateKey.publicKey();
138
152 - walletInfo.address = _walletKeyPair!.address;
139 + walletInfo.address = _solanaPublicKey.toAddress().address;
140
141 await walletAddresses.init();
142 await transactionHistory.init();
143 await save();
144 }
145
159 - Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
146 + Future<SolanaPrivateKey> getPrivateKey({
147 + String? mnemonic,
148 + String? privateKey,
149 + String? passphrase,
150 + }) async {
151 assert(mnemonic != null || privateKey != null);
152
153 if (mnemonic != null) {
163 - return Wallet.fromMnemonic(mnemonic, account: 0, change: 0);
154 + final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase ?? '');
155 +
156 + // Derive a Solana private key from the seed
157 + final bip44 = Bip44.fromSeed(seed, Bip44Coins.solana);
158 +
159 + final childKey = bip44.deriveDefaultPath.change(Bip44Changes.chainExt);
160 +
161 + return SolanaPrivateKey.fromSeed(childKey.privateKey.raw);
162 }
163
164 try {
167 - final privateKeyBytes = base58decode(privateKey!);
168 - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList());
165 + final keypairBytes = Base58Decoder.decode(privateKey!);
166 + return SolanaPrivateKey.fromSeed(keypairBytes);
167 } catch (_) {
168 final privateKeyBytes = HEX.decode(privateKey!);
171 - return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
169 + return SolanaPrivateKey.fromBytes(privateKeyBytes);
170 }
171 }
172
@@ -206,7 +204,8 @@ abstract class SolanaWalletBase
204
205 Future<void> _getEstimatedFees() async {
206 try {
209 - estimatedFee = await _client.getEstimatedFee(_walletKeyPair!);
207 + estimatedFee = await _client.getEstimatedFee(_solanaPublicKey, Commitment.confirmed);
208 + printV(estimatedFee.toString());
209 } catch (e) {
210 estimatedFee = 0.0;
211 }
@@ -274,7 +273,7 @@ abstract class SolanaWalletBase
273 tokenMint: tokenMint,
274 tokenTitle: transactionCurrency.title,
275 inputAmount: totalAmount,
277 - ownerKeypair: _walletKeyPair!,
276 + ownerPrivateKey: _solanaPrivateKey,
277 tokenDecimals: transactionCurrency.decimals,
278 destinationAddress: solCredentials.outputs.first.isParsedAddress
279 ? solCredentials.outputs.first.extractedAddress!
@@ -291,9 +290,7 @@ abstract class SolanaWalletBase
290
291 /// Fetches the native SOL transactions linked to the wallet Public Key
292 Future<void> _updateNativeSOLTransactions() async {
294 - final address = Ed25519HDPublicKey.fromBase58(_walletKeyPair!.address);
295 -
296 - final transactions = await _client.fetchTransactions(address);
293 + final transactions = await _client.fetchTransactions(_solanaPublicKey.toAddress());
294
295 await _addTransactionsToTransactionHistory(transactions);
296 }
@@ -308,10 +305,10 @@ abstract class SolanaWalletBase
305 for (var token in tokenKeys) {
306 if (token is SPLToken) {
307 final tokenTxs = await _client.getSPLTokenTransfers(
311 - token.mintAddress,
312 - token.symbol,
313 - token.decimal,
314 - _walletKeyPair!,
308 + mintAddress: token.mintAddress,
309 + splTokenSymbol: token.symbol,
310 + splTokenDecimal: token.decimal,
311 + privateKey: _solanaPrivateKey,
312 );
313
314 // splTokenTransactions.addAll(tokenTxs);
@@ -387,6 +384,7 @@ abstract class SolanaWalletBase
384 'mnemonic': _mnemonic,
385 'private_key': _hexPrivateKey,
386 'balance': balance[currency]!.toJSON(),
387 + 'passphrase': passphrase,
388 });
389
390 static Future<SolanaWallet> open({
@@ -414,8 +412,9 @@ abstract class SolanaWalletBase
412 if (!hasKeysFile) {
413 final mnemonic = data!['mnemonic'] as String?;
414 final privateKey = data['private_key'] as String?;
415 + final passphrase = data['passphrase'] as String?;
416
418 - keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey);
417 + keysData = WalletKeysData(mnemonic: mnemonic, privateKey: privateKey, passphrase: passphrase);
418 } else {
419 keysData = await WalletKeysFile.readKeysFile(
420 name,
@@ -428,6 +427,7 @@ abstract class SolanaWalletBase
427 return SolanaWallet(
428 walletInfo: walletInfo,
429 password: password,
430 + passphrase: keysData.passphrase,
431 mnemonic: keysData.mnemonic,
432 privateKey: keysData.privateKey,
433 initialBalance: balance,
@@ -442,7 +442,7 @@ abstract class SolanaWalletBase
442 }
443
444 Future<SolanaBalance> _fetchSOLBalance() async {
445 - final balance = await _client.getBalance(_walletKeyPair!.address);
445 + final balance = await _client.getBalance(solanaAddress);
446
447 return SolanaBalance(balance);
448 }
@@ -451,10 +451,9 @@ abstract class SolanaWalletBase
451 for (var token in splTokensBox.values) {
452 if (token.enabled) {
453 try {
454 - final tokenBalance =
455 - await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
456 - balance[token] ??
457 - SolanaBalance(0.0);
454 + final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
455 + balance[token] ??
456 + SolanaBalance(0.0);
457 balance[token] = tokenBalance;
458 } catch (e) {
459 printV('Error fetching spl token (${token.symbol}) balance ${e.toString()}');
@@ -482,10 +481,9 @@ abstract class SolanaWalletBase
481 await splTokensBox.put(token.mintAddress, token);
482
483 if (token.enabled) {
485 - final tokenBalance =
486 - await _client.getSplTokenBalance(token.mintAddress, _walletKeyPair!.address) ??
487 - balance[token] ??
488 - SolanaBalance(0.0);
484 + final tokenBalance = await _client.getSplTokenBalance(token.mintAddress, solanaAddress) ??
485 + balance[token] ??
486 + SolanaBalance(0.0);
487
488 balance[token] = tokenBalance;
489 } else {
@@ -507,37 +505,10 @@ abstract class SolanaWalletBase
505 }
506
507 Future<SPLToken?> getSPLToken(String mintAddress) async {
510 - // Convert SPL token mint address to public key
511 - final Ed25519HDPublicKey mintPublicKey;
508 try {
513 - mintPublicKey = Ed25519HDPublicKey.fromBase58(mintAddress);
514 - } catch (_) {
515 - return null;
516 - }
517 -
518 - // Fetch token's metadata account
519 - try {
520 - final token = await solanaClient!.rpcClient.getMetadata(mint: mintPublicKey);
521 -
522 - if (token == null) {
523 - return null;
524 - }
525 -
526 - String? iconPath;
527 - try {
528 - iconPath = await _client.getIconImageFromTokenUri(token.uri);
529 - } catch (_) {}
530 -
531 - String filteredTokenSymbol = token.symbol.replaceFirst(RegExp('^\\\$'), '');
532 -
533 - return SPLToken.fromMetadata(
534 - name: token.name,
535 - mint: token.mint,
536 - symbol: filteredTokenSymbol,
537 - mintAddress: mintAddress,
538 - iconPath: iconPath,
539 - );
540 - } catch (e) {
509 + return await _client.fetchSPLTokenInfo(mintAddress);
510 + } catch (e, s) {
511 + printV('Error fetching token: ${e.toString()}, ${s.toString()}');
512 return null;
513 }
514 }
@@ -582,7 +553,7 @@ abstract class SolanaWalletBase
553 final messageBytes = utf8.encode(message);
554
555 // Sign the message bytes with the wallet's private key
585 - final signature = (await _walletKeyPair!.sign(messageBytes)).toString();
556 + final signature = (_solanaPrivateKey.sign(messageBytes)).toString();
557
558 return HEX.encode(utf8.encode(signature)).toUpperCase();
559 }
@@ -596,7 +567,7 @@ abstract class SolanaWalletBase
567 final base58EncodedPublicKeyString = match.group(2)!;
568 final sigBytes = bytesString.split(', ').map(int.parse).toList();
569
599 - List<int> pubKeyBytes = base58decode(base58EncodedPublicKeyString);
570 + List<int> pubKeyBytes = SolAddrDecoder().decodeAddr(base58EncodedPublicKeyString);
571
572 return [sigBytes, pubKeyBytes];
573 } else {
@@ -619,19 +590,18 @@ abstract class SolanaWalletBase
590 }
591
592 // make sure the address derived from the public key provided matches the one we expect
622 - final pub = Ed25519HDPublicKey(pubKeyBytes);
623 - if (address != pub.toBase58()) {
593 + final pub = SolanaPublicKey.fromBytes(pubKeyBytes);
594 + if (address != pub.toAddress().address) {
595 return false;
596 }
597
627 - return await verifySignature(
598 + return pub.verify(
599 message: messageBytes,
600 signature: sigBytes,
630 - publicKey: Ed25519HDPublicKey(pubKeyBytes),
601 );
602 }
603
634 - SolanaClient? get solanaClient => _client.getSolanaClient;
604 + SolanaRPC? get solanaProvider => _client.getSolanaProvider;
605
606 @override
607 String get password => _password;
cw_solana/lib/solana_wallet_service.dart
+2
@@ -33,6 +33,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
33 walletInfo: credentials.walletInfo!,
34 mnemonic: mnemonic,
35 password: credentials.password!,
36 + passphrase: credentials.passphrase,
37 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
38 );
39
@@ -118,6 +119,7 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
119 password: credentials.password!,
120 mnemonic: credentials.mnemonic,
121 walletInfo: credentials.walletInfo!,
122 + passphrase: credentials.passphrase,
123 encryptionFileUtils: encryptionFileUtilsFor(isDirect),
124 );
125
cw_solana/lib/spl_token.dart
+1 -30
@@ -1,7 +1,6 @@
1 import 'package:cw_core/crypto_currency.dart';
2 import 'package:cw_core/hive_type_ids.dart';
3 import 'package:hive/hive.dart';
4 -import 'package:solana/metaplex.dart';
4
5 part 'spl_token.g.dart';
6
@@ -55,7 +54,7 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
54 required String mint,
55 required String symbol,
56 required String mintAddress,
58 - String? iconPath
57 + String? iconPath,
58 }) {
59 return SPLToken(
60 name: name,
@@ -117,31 +116,3 @@ class SPLToken extends CryptoCurrency with HiveObjectMixin {
116 @override
117 int get hashCode => mintAddress.hashCode;
118 }
120 -
121 -class NFT extends SPLToken {
122 - final ImageInfo? imageInfo;
123 -
124 - NFT(
125 - String mint,
126 - String name,
127 - String symbol,
128 - String mintAddress,
129 - int decimal,
130 - String iconPath,
131 - this.imageInfo,
132 - ) : super(
133 - name: name,
134 - symbol: symbol,
135 - mintAddress: mintAddress,
136 - decimal: decimal,
137 - mint: mint,
138 - iconPath: iconPath,
139 - );
140 -}
141 -
142 -class ImageInfo {
143 - final String uri;
144 - final OffChainMetadata? data;
145 -
146 - const ImageInfo(this.uri, this.data);
147 -}
cw_solana/pubspec.yaml
+8 -1
@@ -11,7 +11,6 @@ environment:
11 dependencies:
12 flutter:
13 sdk: flutter
14 - solana: ^0.31.0+1
14 cw_core:
15 path: ../cw_core
16 http: ^1.1.0
@@ -21,6 +20,14 @@ dependencies:
20 shared_preferences: ^2.0.15
21 bip32: ^2.0.0
22 hex: ^0.2.0
23 + on_chain:
24 + git:
25 + url: https://github.com/cake-tech/on_chain.git
26 + ref: cake-update-v2
27 + blockchain_utils:
28 + git:
29 + url: https://github.com/cake-tech/blockchain_utils
30 + ref: cake-update-v2
31
32 dev_dependencies:
33 flutter_test:
cw_tron/pubspec.yaml
+1 -1
@@ -17,7 +17,7 @@ dependencies:
17 path: ../cw_evm
18 on_chain:
19 git:
20 - url: https://github.com/cake-tech/On_chain
20 + url: https://github.com/cake-tech/on_chain.git
21 ref: cake-update-v2
22 blockchain_utils:
23 git:
ios/Podfile.lock
-38
@@ -3,38 +3,8 @@ PODS:
3 - Flutter
4 - ReachabilitySwift
5 - CryptoSwift (1.8.3)
6 - - cw_haven (0.0.1):
7 - - cw_haven/Boost (= 0.0.1)
8 - - cw_haven/Haven (= 0.0.1)
9 - - cw_haven/OpenSSL (= 0.0.1)
10 - - cw_haven/Sodium (= 0.0.1)
11 - - cw_shared_external
12 - - Flutter
13 - - cw_haven/Boost (0.0.1):
14 - - cw_shared_external
15 - - Flutter
16 - - cw_haven/Haven (0.0.1):
17 - - cw_shared_external
18 - - Flutter
19 - - cw_haven/OpenSSL (0.0.1):
20 - - cw_shared_external
21 - - Flutter
22 - - cw_haven/Sodium (0.0.1):
23 - - cw_shared_external
24 - - Flutter
6 - cw_mweb (0.0.1):
7 - Flutter
27 - - cw_shared_external (0.0.1):
28 - - cw_shared_external/Boost (= 0.0.1)
29 - - cw_shared_external/OpenSSL (= 0.0.1)
30 - - cw_shared_external/Sodium (= 0.0.1)
31 - - Flutter
32 - - cw_shared_external/Boost (0.0.1):
33 - - Flutter
34 - - cw_shared_external/OpenSSL (0.0.1):
35 - - Flutter
36 - - cw_shared_external/Sodium (0.0.1):
37 - - Flutter
8 - device_display_brightness (0.0.1):
9 - Flutter
10 - device_info_plus (0.0.1):
@@ -136,9 +106,7 @@ PODS:
106 DEPENDENCIES:
107 - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
108 - CryptoSwift
139 - - cw_haven (from `.symlinks/plugins/cw_haven/ios`)
109 - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`)
141 - - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`)
110 - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`)
111 - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
112 - devicelocale (from `.symlinks/plugins/devicelocale/ios`)
@@ -179,12 +147,8 @@ SPEC REPOS:
147 EXTERNAL SOURCES:
148 connectivity_plus:
149 :path: ".symlinks/plugins/connectivity_plus/ios"
182 - cw_haven:
183 - :path: ".symlinks/plugins/cw_haven/ios"
150 cw_mweb:
151 :path: ".symlinks/plugins/cw_mweb/ios"
186 - cw_shared_external:
187 - :path: ".symlinks/plugins/cw_shared_external/ios"
152 device_display_brightness:
153 :path: ".symlinks/plugins/device_display_brightness/ios"
154 device_info_plus:
@@ -239,9 +203,7 @@ EXTERNAL SOURCES:
203 SPEC CHECKSUMS:
204 connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
205 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483
242 - cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a
206 cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3
244 - cw_shared_external: 2972d872b8917603478117c9957dfca611845a92
207 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
208 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
209 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926
lib/core/wallet_connect/chain_service/solana/solana_chain_service.dart
+22 -32
@@ -1,5 +1,7 @@
1 +import 'dart:convert';
2 import 'dart:developer';
3
4 +import 'package:blockchain_utils/blockchain_utils.dart';
5 import 'package:cake_wallet/core/wallet_connect/chain_service/solana/entities/solana_sign_message.dart';
6 import 'package:cake_wallet/core/wallet_connect/chain_service/solana/solana_chain_id.dart';
7 import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart';
@@ -8,9 +10,9 @@ import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_w
10 import 'package:cake_wallet/core/wallet_connect/models/connection_model.dart';
11 import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget.dart';
12 import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart';
13 +import 'package:cw_core/solana_rpc_http_service.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
12 -import 'package:solana/base58.dart';
13 -import 'package:solana/solana.dart';
15 +import 'package:on_chain/solana/solana.dart';
16 import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
17 import '../chain_service.dart';
18 import '../../wallet_connect_key_service.dart';
@@ -27,25 +29,19 @@ class SolanaChainServiceImpl implements ChainService {
29
30 final SolanaChainId reference;
31
30 - final SolanaClient solanaClient;
32 + final SolanaRPC solanaProvider;
33
32 - final Ed25519HDKeyPair? ownerKeyPair;
34 + final SolanaPrivateKey? ownerPrivateKey;
35
36 SolanaChainServiceImpl({
37 required this.reference,
38 required this.wcKeyService,
39 required this.bottomSheetService,
40 required this.wallet,
39 - required this.ownerKeyPair,
40 - required String webSocketUrl,
41 - required Uri rpcUrl,
42 - SolanaClient? solanaClient,
43 - }) : solanaClient = solanaClient ??
44 - SolanaClient(
45 - rpcUrl: rpcUrl,
46 - websocketUrl: Uri.parse(webSocketUrl),
47 - timeout: const Duration(minutes: 5),
48 - ) {
41 + required this.ownerPrivateKey,
42 + required String formattedRPCUrl,
43 + SolanaRPC? solanaProvider,
44 + }) : solanaProvider = solanaProvider ?? SolanaRPC(SolanaRPCHTTPService(url: formattedRPCUrl)) {
45 for (final String event in getEvents()) {
46 wallet.registerEventEmitter(chainId: getChainId(), event: event);
47 }
@@ -110,26 +106,20 @@ class SolanaChainServiceImpl implements ChainService {
106 }
107
108 try {
113 - final message =
114 - await solanaClient.rpcClient.getMessageFromEncodedTx(solanaSignTx.transaction);
109 + // Convert transaction string to bytes
110 + List<int> transactionBytes = base64Decode(solanaSignTx.transaction);
111
116 - final sign = await ownerKeyPair?.signMessage(
117 - message: message,
118 - recentBlockhash: solanaSignTx.recentBlockhash ?? '',
119 - );
112 + final message = SolanaTransactionUtils.deserializeMessageLegacy(transactionBytes);
113
121 - if (sign == null) {
122 - return '';
123 - }
114 + final sign = ownerPrivateKey!.sign(message.serialize());
115
125 - String signature = await solanaClient.sendAndConfirmTransaction(
126 - message: message,
127 - signers: [ownerKeyPair!],
128 - commitment: Commitment.confirmed,
116 + final signature = solanaProvider.request(
117 + SolanaRPCSendTransaction(
118 + encodedTransaction: Base58Encoder.encode(sign),
119 + commitment: Commitment.confirmed,
120 + ),
121 );
122
131 - printV(signature);
132 -
123 bottomSheetService.queueBottomSheet(
124 isModalDismissible: true,
125 widget: BottomSheetMessageDisplayWidget(
@@ -161,10 +151,10 @@ class SolanaChainServiceImpl implements ChainService {
151 if (authError != null) {
152 return authError;
153 }
164 - Signature? sign;
154 + List<int>? sign;
155
156 try {
167 - sign = await ownerKeyPair?.sign(base58decode(solanaSignMessage.message));
157 + sign = ownerPrivateKey!.sign(Base58Decoder.decode(solanaSignMessage.message));
158 } catch (e) {
159 printV(e);
160 }
@@ -173,7 +163,7 @@ class SolanaChainServiceImpl implements ChainService {
163 return '';
164 }
165
176 - String signature = sign.toBase58();
166 + final signature = Base58Encoder.encode(sign);
167
168 return signature;
169 }
lib/core/wallet_connect/web3wallet_service.dart
+9 -9
@@ -22,6 +22,7 @@ import 'package:cw_core/wallet_type.dart';
22 import 'package:eth_sig_util/eth_sig_util.dart';
23 import 'package:flutter/material.dart';
24 import 'package:mobx/mobx.dart';
25 +import 'package:on_chain/solana/solana.dart' hide Store;
26 import 'package:shared_preferences/shared_preferences.dart';
27 import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart';
28
@@ -140,29 +141,28 @@ abstract class Web3WalletServiceBase with Store {
141 for (final cId in SolanaChainId.values) {
142 final node = appStore.settingsStore.getCurrentNode(appStore.wallet!.type);
143
143 - Uri rpcUri = node.uri;
144 - String webSocketUrl = 'wss://${node.uriRaw}';
144 + String formattedUrl;
145 + String protocolUsed = node.isSSL ? "https" : "http";
146
147 if (node.uriRaw == 'rpc.ankr.com') {
148 String ankrApiKey = secrets.ankrApiKey;
149
149 - rpcUri = Uri.https(node.uriRaw, '/solana/$ankrApiKey');
150 - webSocketUrl = 'wss://${node.uriRaw}/solana/ws/$ankrApiKey';
150 + formattedUrl = '$protocolUsed://${node.uriRaw}/$ankrApiKey';
151 } else if (node.uriRaw == 'solana-mainnet.core.chainstack.com') {
152 String chainStackApiKey = secrets.chainStackApiKey;
153
154 - rpcUri = Uri.https(node.uriRaw, '/$chainStackApiKey');
155 - webSocketUrl = 'wss://${node.uriRaw}/$chainStackApiKey';
154 + formattedUrl = '$protocolUsed://${node.uriRaw}/$chainStackApiKey';
155 + } else {
156 + formattedUrl = '$protocolUsed://${node.uriRaw}';
157 }
158
159 SolanaChainServiceImpl(
160 reference: cId,
160 - rpcUrl: rpcUri,
161 - webSocketUrl: webSocketUrl,
161 + formattedRPCUrl: formattedUrl,
162 wcKeyService: walletKeyService,
163 bottomSheetService: _bottomSheetHandler,
164 wallet: _web3Wallet,
165 - ownerKeyPair: solana!.getWalletKeyPair(appStore.wallet!),
165 + ownerPrivateKey: SolanaPrivateKey.fromSeedHex(solana!.getPrivateKey(appStore.wallet!)),
166 );
167 }
168 }
lib/solana/cw_solana.dart
+2 -5
@@ -52,11 +52,8 @@ class CWSolana extends Solana {
52 String getPrivateKey(WalletBase wallet) => (wallet as SolanaWallet).privateKey;
53
54 @override
55 - String getPublicKey(WalletBase wallet) => (wallet as SolanaWallet).keys.publicKey.toBase58();
56 -
57 - @override
58 - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet) => (wallet as SolanaWallet).walletKeyPair;
59 -
55 + String getPublicKey(WalletBase wallet) =>
56 + (wallet as SolanaWallet).solanaPublicKey.toAddress().address;
57 Object createSolanaTransactionCredentials(
58 List<Output> outputs, {
59 required CryptoCurrency currency,
lib/src/screens/wallet_keys/wallet_keys_page.dart
+1 -1
@@ -326,7 +326,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
326 ),
327 );
328 }
329 -
329 +
330 Widget _buildBottomActionPanel({
331 required String titleForClipboard,
332 required String dataToCopy,
lib/view_model/advanced_privacy_settings_view_model.dart
+1
@@ -78,6 +78,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
78 WalletType.ethereum,
79 WalletType.polygon,
80 WalletType.tron,
81 + WalletType.solana,
82 WalletType.monero,
83 WalletType.wownero,
84 WalletType.zano,
pubspec_base.yaml
+8 -1
@@ -106,12 +106,19 @@ dependencies:
106 flutter_svg: ^2.0.9
107 polyseed: ^0.0.6
108 nostr_tools: ^1.0.9
109 - solana: ^0.31.0+1
109 ledger_flutter_plus:
110 git:
111 url: https://github.com/vespr-wallet/ledger-flutter-plus
112 ref: c2e341d8038f1108690ad6f80f7b4b7156aacc76
113 hashlib: ^1.19.2
114 + on_chain:
115 + git:
116 + url: https://github.com/cake-tech/on_chain.git
117 + ref: cake-update-v2
118 + blockchain_utils:
119 + git:
120 + url: https://github.com/cake-tech/blockchain_utils
121 + ref: cake-update-v2
122
123 dev_dependencies:
124 flutter_test:
tool/configure.dart
-2
@@ -1261,7 +1261,6 @@ import 'package:cw_core/wallet_credentials.dart';
1261 import 'package:cw_core/wallet_info.dart';
1262 import 'package:cw_core/wallet_service.dart';
1263 import 'package:hive/hive.dart';
1264 -import 'package:solana/solana.dart';
1264
1265 """;
1266 const solanaCWHeaders = """
@@ -1289,7 +1288,6 @@ abstract class Solana {
1288 String getAddress(WalletBase wallet);
1289 String getPrivateKey(WalletBase wallet);
1290 String getPublicKey(WalletBase wallet);
1292 - Ed25519HDKeyPair? getWalletKeyPair(WalletBase wallet);
1291
1292 Object createSolanaTransactionCredentials(
1293 List<Output> outputs, {