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 (_) {
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
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,
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,
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
);
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
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
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,
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
);
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,
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