3
4
import 'package:blockchain_utils/blockchain_utils.dart';
5
import 'package:cw_core/amount/money.dart';
6
-import 'package:cw_core/amount/money_double.dart';
6
import 'package:cw_core/crypto_currency.dart';
8
-import 'package:cw_core/currency.dart';
7
import 'package:cw_core/node.dart';
8
import 'package:cw_core/utils/proxy_wrapper.dart';
9
import 'package:cw_core/solana_rpc_http_service.dart';
213
// For native solana transactions
214
if (instruction.accounts.length < 2) continue;
215
218
- // Get the fee payer index based on transaction type
219
- // For legacy transfers, the first account is usually the fee payer
220
- // For versioned, the first account in instruction is usually the fee payer
221
- final feePayerIndex =
222
- txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0];
223
-
216
final transactionModel = await _parseNativeTransaction(
217
message: message,
218
meta: meta,
219
fee: fee,
228
- feePayerIndex: feePayerIndex,
220
walletAddress: walletAddress,
221
signature: signature,
222
blockTime: blockTime,
225
if (transactionModel != null) {
226
return [transactionModel];
227
}
237
- } else if (programId == SPLTokenProgramConst.tokenProgramId) {
238
- // For SPL Token transactions
228
+ } else if (programId == SPLTokenProgramConst.tokenProgramId ||
229
+ programId == SPLTokenProgramConst.token2022ProgramId) {
230
if (instruction.accounts.length < 2) continue;
231
232
final transactionModel = await _parseSPLTokenTransaction(
256
bool hasTokenTransfer = false;
257
for (final otherInstruction in instructions) {
258
final otherProgramId = message.accountKeys[otherInstruction.programIdIndex];
268
- if (otherProgramId == SPLTokenProgramConst.tokenProgramId) {
259
+ if (otherProgramId == SPLTokenProgramConst.tokenProgramId ||
260
+ otherProgramId == SPLTokenProgramConst.token2022ProgramId) {
261
hasTokenTransfer = true;
262
break;
263
}
381
return walletSent && walletReceived;
382
}
383
384
+ static CryptoCurrency currencyForRawAmount(SPLToken? token, int mintDecimals) {
385
+ if (token != null && token.decimals == mintDecimals) {
386
+ return token;
387
+ }
388
+
389
+ return CryptoCurrency(
390
+ name: (token?.title ?? "TOKEN").toLowerCase(),
391
+ title: token?.title ?? "TOKEN",
392
+ decimals: mintDecimals,
393
+ );
394
+ }
395
+
396
/// Parses a swap transaction and creates dual entries (outgoing and incoming)
397
Future<List<SolanaTransactionModel>> _parseSwapTransaction({
398
required VersionedMessage message,
411
final preTokenBalances = meta.preTokenBalances;
412
final postTokenBalances = meta.postTokenBalances;
413
414
+ final walletPaidFee = accountKeys.isNotEmpty && accountKeys.first.address == walletAddress;
415
+ final feeAdjustment = walletPaidFee ? BigInt.from(fee) : BigInt.zero;
416
+
417
String? decreasedMintForWallet;
418
String? increasedMintForWallet;
419
449
decreasedMintForWallet != increasedMintForWallet;
450
451
// Parse outgoing side (what was sent)
445
- double outgoingAmount = 0.0;
446
- Currency outgoingToken = CryptoCurrency.sol;
452
+ Money? outgoingMoney;
453
String? outgoingMintAddress;
454
String? outgoingFrom;
455
String? outgoingTo;
467
if (accountAddress == walletAddress) {
468
final preBalance = preBalances[i];
469
final postBalance = postBalances[i];
464
- final balanceChange = preBalance - postBalance;
470
+
471
+ final balanceChange = preBalance - postBalance - feeAdjustment;
472
473
if (balanceChange > BigInt.zero) {
474
// The wallet sent SOL
468
- outgoingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
469
- outgoingToken = CryptoCurrency.sol;
475
+ outgoingMoney = Money(balanceChange, CryptoCurrency.sol);
476
outgoingMintAddress = null;
477
outgoingFrom = walletAddress;
478
// We find the intermediate account or swap program account
490
}
491
492
// If no SOL outgoing, we check if there are any SPL token balance changes for the wallet
487
- if (outgoingAmount == 0.0 && preTokenBalances != null) {
493
+ if (outgoingMoney == null && preTokenBalances != null && postTokenBalances != null) {
494
for (final preTokenBal in preTokenBalances) {
495
final owner = preTokenBal.owner?.address ?? '';
496
500
if (isSplToSplSwap && mint != decreasedMintForWallet) {
501
continue;
502
}
497
- final preAmount = preTokenBal.uiTokenAmount.uiAmount ?? 0.0;
503
+ final preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
504
505
// We find the corresponding post balance
500
- for (final postTokenBal in postTokenBalances ?? []) {
506
+ for (final postTokenBal in postTokenBalances) {
507
final postOwner = postTokenBal.owner?.address ?? '';
508
final postMint = postTokenBal.mint.address;
503
- final postAmount = postTokenBal.uiTokenAmount.uiAmount ?? 0.0;
509
510
if (postOwner == walletAddress && postMint == mint) {
506
- final diff = preAmount - postAmount;
511
+ final postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
512
+ final diff = preRaw - postRaw;
513
508
- if (diff > 0) {
514
+ if (diff > BigInt.zero) {
515
// The wallet sent tokens
510
- outgoingAmount = diff.toDouble();
511
- outgoingMintAddress = mint;
516
final token = await getTokenInfo(mint);
513
- outgoingToken =
514
- token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
517
+ outgoingMoney =
518
+ Money(diff, currencyForRawAmount(token, preTokenBal.uiTokenAmount.decimals));
519
+ outgoingMintAddress = mint;
520
outgoingFrom = walletAddress;
521
// We find the intermediate account
522
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
531
}
532
}
533
529
- if (outgoingAmount > 0) break;
534
+ if (outgoingMoney != null) {
535
+ break;
536
+ }
537
}
538
}
539
}
540
541
// Parse incoming side (what was received)
535
- double incomingAmount = 0.0;
536
- Currency incomingToken = CryptoCurrency.sol;
542
+ Money? incomingMoney;
543
String? incomingMintAddress;
544
String? incomingFrom;
545
String? incomingTo;
556
if (accountAddress == walletAddress) {
557
final preBalance = preBalances[i];
558
final postBalance = postBalances[i];
553
- final balanceChange = postBalance - preBalance;
559
+ final balanceChange = postBalance - preBalance + feeAdjustment;
560
561
if (balanceChange > BigInt.zero) {
562
// The wallet received SOL
557
- incomingAmount = balanceChange.toDouble() / SolanaUtils.lamportsPerSol;
558
- incomingToken = CryptoCurrency.sol;
563
+ incomingMoney = Money(balanceChange, CryptoCurrency.sol);
564
incomingMintAddress = null;
565
incomingTo = walletAddress;
566
// We find the intermediate account
578
}
579
580
// If no SOL incoming, check SPL token incoming using ATA derivation
576
- if (incomingAmount == 0.0 && preTokenBalances != null && postTokenBalances != null) {
581
+ if (incomingMoney == null && preTokenBalances != null && postTokenBalances != null) {
582
// Collect all unique mints from token balances (excluding wrapped SOL)
583
final mints = <String>{};
584
for (final tokenBal in preTokenBalances) {
600
final walletSolAddress = SolAddress(walletAddress);
601
final mintSolAddress = SolAddress(mint);
602
598
- final ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
603
+ final standardAta = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
604
mint: mintSolAddress,
605
owner: walletSolAddress,
606
);
602
- final ataAddress = ata.address.address;
607
+ final token2022Ata = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
608
+ mint: mintSolAddress,
609
+ owner: walletSolAddress,
610
+ tokenProgramId: SPLTokenProgramConst.token2022ProgramId,
611
+ );
612
+ final ataAddresses = [standardAta.address.address, token2022Ata.address.address];
613
604
- // We check if this ATA address appears in the account keys
614
+ // We check if either ATA address appears in the account keys
615
int? ataAccountIndex;
616
for (int i = 0; i < accountKeys.length; i++) {
617
final accountKey = accountKeys[i];
608
- if (accountKey.address == ataAddress) {
618
+ if (ataAddresses.contains(accountKey.address)) {
619
ataAccountIndex = i;
620
break;
621
}
623
624
// If ATA is in the transaction, we check for balance changes
625
if (ataAccountIndex != null) {
616
- double preAmount = 0.0;
617
- double postAmount = 0.0;
626
+ BigInt preRaw = BigInt.zero;
627
+ BigInt postRaw = BigInt.zero;
628
+ int? mintDecimals;
629
630
// We find the pre balance
631
for (final preTokenBal in preTokenBalances) {
632
final accountIndex = preTokenBal.accountIndex;
633
final tokenMint = preTokenBal.mint.address;
634
if (accountIndex == ataAccountIndex && tokenMint == mint) {
624
- preAmount = preTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
635
+ preRaw = BigInt.tryParse(preTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
636
+ mintDecimals = preTokenBal.uiTokenAmount.decimals;
637
break;
638
}
639
}
643
final accountIndex = postTokenBal.accountIndex;
644
final tokenMint = postTokenBal.mint.address;
645
if (accountIndex == ataAccountIndex && tokenMint == mint) {
634
- postAmount = postTokenBal.uiTokenAmount.uiAmount?.toDouble() ?? 0.0;
646
+ postRaw = BigInt.tryParse(postTokenBal.uiTokenAmount.amount) ?? BigInt.zero;
647
+ mintDecimals = postTokenBal.uiTokenAmount.decimals;
648
break;
649
}
650
}
651
639
- final diff = postAmount - preAmount;
640
- if (diff > 0) {
652
+ final diff = postRaw - preRaw;
653
+ if (diff > BigInt.zero && mintDecimals != null) {
654
// The wallet received tokens
642
- incomingAmount = diff.toDouble();
643
- incomingMintAddress = mint;
655
final token = await getTokenInfo(mint);
645
- incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6);
656
+ incomingMoney = Money(diff, currencyForRawAmount(token, mintDecimals));
657
+ incomingMintAddress = mint;
658
incomingTo = walletAddress;
659
// We find the intermediate account
660
if (instructions.isNotEmpty && instructions[0].accounts.isNotEmpty) {
675
}
676
677
// Outgoing transaction model
666
- if (outgoingAmount > 0.0 && outgoingFrom != null && outgoingTo != null) {
678
+ if (outgoingMoney != null && outgoingFrom != null && outgoingTo != null) {
679
final outgoingId =
680
'${signature}_outgoing'; // We create a composite ID for the outgoing transaction
681
swapTransactions.add(SolanaTransactionModel(
683
from: outgoingFrom,
684
to: outgoingTo,
685
id: outgoingId,
674
- amount: outgoingAmount.toMoney(outgoingToken),
686
+ amount: outgoingMoney,
687
programId: outgoingMintAddress == null
688
? SystemProgramConst.programId.address
689
: SPLTokenProgramConst.tokenProgramId.address,
693
}
694
695
// Incoming transaction model
684
- if (incomingAmount > 0.0 && incomingFrom != null && incomingTo != null) {
696
+ if (incomingMoney != null && incomingFrom != null && incomingTo != null) {
697
final incomingId =
698
'${signature}_incoming'; // We create a composite ID for the incoming transaction
699
swapTransactions.add(SolanaTransactionModel(
701
from: incomingFrom,
702
to: incomingTo,
703
id: incomingId,
692
- amount: incomingAmount.toMoney(incomingToken),
704
+ amount: incomingMoney,
705
programId: incomingMintAddress == null
706
? SystemProgramConst.programId.address
707
: SPLTokenProgramConst.tokenProgramId.address,
717
required VersionedMessage message,
718
required ConfirmedTransactionMeta meta,
719
required int fee,
708
- required int feePayerIndex,
720
required String walletAddress,
721
required String signature,
722
required BigInt? blockTime,
747
// Positive = wallet lost SOL, negative = wallet gained.
748
final walletChange = walletPre - walletPost;
749
739
- final bool walletPaidFee =
740
- feePayerIndex < accountKeys.length && accountKeys[feePayerIndex].address == walletAddress;
750
+ final walletPaidFee = accountKeys.first.address == walletAddress;
751
752
// Net transfer amount excluding the fee.
753
final netChange = walletPaidFee ? walletChange - BigInt.from(fee) : walletChange;
818
mintAddress = accountKeys[accounts[1]].address;
819
}
820
811
- double userPreAmount = 0.0;
812
- double userPostAmount = 0.0;
821
+ BigInt userPreRaw = BigInt.zero;
822
+ BigInt userPostRaw = BigInt.zero;
823
+ int? mintDecimals;
824
825
if (preTokenBalances != null) {
826
for (final preBal in preTokenBalances) {
831
continue;
832
}
833
mintAddress ??= preBal.mint.address;
823
- userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
834
+ mintDecimals = preBal.uiTokenAmount.decimals;
835
+ userPreRaw = BigInt.tryParse(preBal.uiTokenAmount.amount) ?? BigInt.zero;
836
break;
837
}
838
}
848
continue;
849
}
850
mintAddress ??= postBal.mint.address;
839
- userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
851
+ mintDecimals = postBal.uiTokenAmount.decimals;
852
+ userPostRaw = BigInt.tryParse(postBal.uiTokenAmount.amount) ?? BigInt.zero;
853
break;
854
}
855
}
856
}
857
}
858
846
- final diff = userPreAmount - userPostAmount;
847
- final rawAmount = diff.abs();
859
+ final diff = userPreRaw - userPostRaw;
860
849
- final amountInString = rawAmount.toStringAsFixed(6);
850
- final amount = double.parse(amountInString);
851
- final isOutgoing = diff > 0;
861
+ if (diff == BigInt.zero || mintDecimals == null) {
862
+ return null;
863
+ }
864
+
865
+ final isOutgoing = diff > BigInt.zero;
866
867
// Resolve sender/receiver from token balance owners
868
String? senderOwner;
900
from: sender,
901
to: receiver,
902
id: signature,
889
- amount: amount.toMoney(splToken ?? CryptoCurrency.sol),
903
+ amount: Money(diff.abs(), currencyForRawAmount(splToken, mintDecimals)),
904
programId: SPLTokenProgramConst.tokenProgramId.address,
905
blockTimeInInt: blockTime?.toInt() ?? 0,
906
fee: Money.fromInt(fee, CryptoCurrency.sol),
1027
final transactions = <SolanaTransactionModel>[];
1028
1029
try {
1016
- final signatures =
1017
- await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
1030
+ final signatures = await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment);
1031
1032
if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions);
1033
1109
1110
if (associatedTokenAccount == null) {
1111
try {
1099
- associatedTokenAccount = await _getOrCreateAssociatedTokenAccount(
1100
- payerPrivateKey: privateKey,
1112
+ associatedTokenAccount = await _findAssociatedTokenAccount(
1113
mintAddress: SolAddress(mintAddress),
1114
ownerAddress: ownerWalletAddress,
1103
- shouldCreateATA: false,
1115
);
1116
} catch (e, s) {
1117
printV('$e \n $s');
1133
);
1134
}
1135
1125
- final Map<String, SPLToken?> tokenInfoCache = {};
1136
+ final Map<String, SPLToken> tokenInfoCache = {};
1137
1138
Future<SPLToken?> getTokenInfo(String mintAddress) async {
1128
- if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress];
1139
+ final cached = tokenInfoCache[mintAddress];
1140
+ if (cached != null) {
1141
+ return cached;
1142
+ }
1143
+
1144
+ final fetched = await fetchSPLTokenInfo(mintAddress);
1145
+ if (fetched != null) {
1146
+ tokenInfoCache[mintAddress] = fetched;
1147
+ }
1148
1130
- return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress);
1149
+ return fetched;
1150
+ }
1151
+
1152
+ Future<int?> _fetchMintDecimals(String mintAddress) async {
1153
+ try {
1154
+ final supply = await _provider!.request(
1155
+ SolanaRPCGetTokenSupply(account: SolAddress(mintAddress)),
1156
+ );
1157
+
1158
+ return supply.decimals;
1159
+ } catch (e) {
1160
+ printV("Could not read decimals for mint $mintAddress: ${e.toString()}");
1161
+ return null;
1162
+ }
1163
}
1164
1165
Future<SPLToken?> fetchSPLTokenInfo(String mintAddress) async {
1182
1183
final symbol = decodedResponse['symbol'] ?? '';
1184
final name = decodedResponse['name'] ?? '';
1153
- final decimal = decodedResponse['decimals'] ?? '0';
1185
+ final rawDecimals = decodedResponse["decimals"];
1186
final iconPath = decodedResponse['logo'] ?? '';
1187
1188
final filteredTokenSymbol = symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1189
1190
+ final reportedDecimals =
1191
+ rawDecimals is num ? rawDecimals.toInt() : int.tryParse("${rawDecimals ?? ""}");
1192
+
1193
+ final decimals = (reportedDecimals != null && reportedDecimals > 0)
1194
+ ? reportedDecimals
1195
+ : await _fetchMintDecimals(mintAddress);
1196
+
1197
+ if (decimals == null) {
1198
+ return null;
1199
+ }
1200
+
1201
return SPLToken(
1202
name: name,
1203
mint: symbol,
1204
symbol: filteredTokenSymbol,
1205
mintAddress: mintAddress,
1206
iconPath: iconPath,
1164
- decimal: int.tryParse(decimal) ?? 0,
1207
+ decimal: decimals,
1208
);
1209
} catch (e, s) {
1210
printV('Error fetching token info: $e \n $s');
1232
String filteredTokenSymbol =
1233
metadata.symbol.replaceFirst(RegExp('^\\\$'), '').replaceAll('\u0000', '');
1234
1235
+ final decimals = await _fetchMintDecimals(token.mint.address);
1236
+
1237
+ if (decimals == null) {
1238
+ return null;
1239
+ }
1240
+
1241
return SPLToken.fromMetadata(
1242
name: metadata.name,
1243
mint: metadata.symbol,
1244
symbol: filteredTokenSymbol,
1245
mintAddress: token.mint.address,
1246
+ decimal: decimals,
1247
iconPath: iconPath,
1248
);
1249
} catch (_) {}
1267
}) async {
1268
const commitment = Commitment.confirmed;
1269
1220
- if (inputAmount.currency == CryptoCurrency.sol) {
1270
+ if (tokenMint == null) {
1271
return _signNativeTokenTransaction(
1272
inputAmount: inputAmount,
1273
destinationAddress: destinationAddress,
1279
} else {
1280
return _signSPLTokenTransaction(
1281
tokenDecimals: inputAmount.currency.decimals,
1232
- tokenMint: tokenMint!,
1282
+ tokenMint: tokenMint,
1283
inputAmount: inputAmount,
1284
ownerPrivateKey: ownerPrivateKey,
1285
destinationAddress: destinationAddress,
1320
);
1321
}
1322
1273
- Future<Message> _getMessageForSPLTokenTransaction({
1274
- required SolAddress ownerAddress,
1275
- required SolAddress destinationAddress,
1276
- required int tokenDecimals,
1277
- required SolAddress mintAddress,
1278
- required SolAddress sourceAccount,
1279
- required Money amount,
1280
- required Commitment commitment,
1281
- required SolAddress tokenProgramId,
1282
- }) async {
1283
- final instructions = [
1284
- SPLTokenProgram.transferChecked(
1285
- layout: SPLTokenTransferCheckedLayout(
1286
- amount: amount.amount,
1287
- decimals: tokenDecimals,
1288
- ),
1289
- mint: mintAddress,
1290
- source: sourceAccount,
1291
- destination: destinationAddress,
1292
- owner: ownerAddress,
1293
- )
1294
- ];
1295
-
1296
- final latestBlockhash = await _getLatestBlockhash(commitment);
1297
-
1298
- return Message.compile(
1299
- transactionInstructions: instructions,
1300
- payer: ownerAddress,
1301
- recentBlockhash: latestBlockhash,
1302
- );
1303
- }
1304
-
1323
Future<Money> _getFeeFromCompiledMessage(Message message, Commitment commitment) {
1324
final base64Message = base64Encode(message.serialize());
1325
return getFeeForMessage(base64Message, commitment);
1326
}
1327
1328
+ Future<Money> _getRentExemptionAmount(int space) async {
1329
+ final rent = await _provider!.request(
1330
+ SolanaRPCGetMinimumBalanceForRentExemption(size: space),
1331
+ );
1332
+
1333
+ return Money(rent, CryptoCurrency.sol);
1334
+ }
1335
+
1336
Future<bool> hasSufficientFundsLeftForRent({
1311
- required Money inputAmount,
1337
+ required Money totalOutflow,
1338
required Money solBalance,
1313
- required Money fee,
1339
}) async {
1315
- final rent = await _provider!.request(
1340
+ final rentBuffer = await _provider!.request(
1341
SolanaRPCGetMinimumBalanceForRentExemption(size: SolanaTokenAccountUtils.accountSize),
1342
);
1343
1319
- return (solBalance - (inputAmount + fee)) > Money(rent, CryptoCurrency.sol);
1344
+ return (solBalance - totalOutflow) > Money(rentBuffer, CryptoCurrency.sol);
1345
}
1346
1347
Future<PendingSolanaTransaction> _signNativeTokenTransaction({
1368
1369
if (!isSendAll) {
1370
final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1346
- inputAmount: inputAmount,
1347
- fee: fee,
1371
+ totalOutflow: inputAmount + fee,
1372
solBalance: solBalance,
1373
);
1374
1381
if (isSendAll) {
1382
final updatedLamports = inputAmount - fee;
1383
1384
+ if (updatedLamports.isNegative || updatedLamports.isZero) {
1385
+ throw SolanaTransactionWrongBalanceException(CryptoCurrency.sol);
1386
+ }
1387
+
1388
final transaction = _constructNativeTransaction(
1389
ownerPrivateKey: ownerPrivateKey,
1390
destinationAddress: destinationAddress,
1530
return SPLTokenProgramConst.tokenProgramId;
1531
}
1532
1505
- Future<ProgramDerivedAddress?> _getOrCreateAssociatedTokenAccount({
1506
- required SolanaPrivateKey payerPrivateKey,
1533
+ Future<ProgramDerivedAddress?> _findAssociatedTokenAccount({
1534
required SolAddress ownerAddress,
1535
required SolAddress mintAddress,
1509
- required bool shouldCreateATA,
1536
}) async {
1511
- // For transaction history loading (shouldCreateATA: false), try standard token program first
1512
- // to avoid unnecessary RPC call. Only fetch token program ID when creating accounts.
1513
- SolAddress tokenProgramId = SPLTokenProgramConst.tokenProgramId;
1514
-
1515
- if (shouldCreateATA) {
1516
- // Only fetch token program ID when we need to create an account
1517
- tokenProgramId = await _getTokenProgramId(mintAddress);
1518
- }
1519
-
1537
// Try with standard token program first (most common case)
1538
var associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1539
mint: mintAddress,
1556
// If account exists with standard program, return it
1557
if (accountInfo != null) return associatedTokenAccount;
1558
1542
- // If not found and we're not creating, try Token-2022 as fallback
1543
- if (!shouldCreateATA) {
1544
- try {
1545
- final token2022ProgramId = await _getTokenProgramId(mintAddress);
1546
- if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
1547
- associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1548
- mint: mintAddress,
1549
- owner: ownerAddress,
1550
- tokenProgramId: token2022ProgramId,
1551
- );
1552
-
1553
- try {
1554
- accountInfo = await _provider!.request(
1555
- SolanaRPCGetAccountInfo(
1556
- account: associatedTokenAccount.address,
1557
- commitment: Commitment.confirmed,
1558
- ),
1559
- );
1560
- if (accountInfo != null) return associatedTokenAccount;
1561
- } catch (_) {}
1562
- }
1563
- } catch (_) {}
1564
- return null;
1565
- }
1566
-
1567
- // For account creation, use the detected token program ID
1568
- associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1569
- mint: mintAddress,
1570
- owner: ownerAddress,
1571
- tokenProgramId: tokenProgramId,
1572
- );
1573
-
1574
- final payerAddress = payerPrivateKey.publicKey().toAddress();
1575
-
1576
- final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount(
1577
- payer: payerAddress,
1578
- associatedToken: associatedTokenAccount.address,
1579
- owner: ownerAddress,
1580
- mint: mintAddress,
1581
- tokenProgramId: tokenProgramId,
1582
- );
1583
-
1584
- final blockhash = await _getLatestBlockhash(Commitment.confirmed);
1585
-
1586
- final transaction = SolanaTransaction(
1587
- payerKey: payerAddress,
1588
- instructions: [createAssociatedTokenAccount],
1589
- recentBlockhash: blockhash,
1590
- type: TransactionType.v0,
1591
- );
1592
-
1593
- final serializedTransaction = await _signTransactionInternal(
1594
- ownerPrivateKey: payerPrivateKey,
1595
- transaction: transaction,
1596
- );
1597
-
1598
- await sendTransaction(
1599
- serializedTransaction: serializedTransaction,
1600
- commitment: Commitment.confirmed,
1601
- );
1559
+ // if its not found under the standard program, then we try Token-2022, which derives a different address
1560
+ try {
1561
+ final token2022ProgramId = await _getTokenProgramId(mintAddress);
1562
+ if (token2022ProgramId.address != SPLTokenProgramConst.tokenProgramId.address) {
1563
+ associatedTokenAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1564
+ mint: mintAddress,
1565
+ owner: ownerAddress,
1566
+ tokenProgramId: token2022ProgramId,
1567
+ );
1568
1603
- // Wait for confirmation
1604
- await Future.delayed(const Duration(seconds: 2));
1569
+ try {
1570
+ accountInfo = await _provider!.request(
1571
+ SolanaRPCGetAccountInfo(
1572
+ account: associatedTokenAccount.address,
1573
+ commitment: Commitment.confirmed,
1574
+ ),
1575
+ );
1576
+ if (accountInfo != null) return associatedTokenAccount;
1577
+ } catch (_) {}
1578
+ }
1579
+ } catch (_) {}
1580
1606
- return associatedTokenAccount;
1581
+ return null;
1582
}
1583
1584
Future<PendingSolanaTransaction> _signSPLTokenTransaction({
1595
1596
ProgramDerivedAddress? associatedSenderAccount;
1597
SolAddress senderTokenProgramId = tokenProgramId;
1598
+ int? senderAccountSpace;
1599
1600
try {
1601
associatedSenderAccount = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1614
1615
if (accountInfo != null) {
1616
senderTokenProgramId = accountInfo.owner;
1617
+ senderAccountSpace = accountInfo.space;
1618
} else {
1619
associatedSenderAccount = null;
1620
}
1641
1642
if (accountInfo != null) {
1643
senderTokenProgramId = accountInfo.owner;
1644
+ senderAccountSpace = accountInfo.space;
1645
} else {
1646
associatedSenderAccount = null;
1647
}
1657
);
1658
}
1659
1682
- // Get or create recipient account using the sender's token program ID
1683
- // This ensures both accounts use the same program
1684
- ProgramDerivedAddress? associatedRecipientAccount;
1660
+ final ProgramDerivedAddress associatedRecipientAccount;
1661
+ bool shouldCreateRecipientAccount = false;
1662
+
1663
try {
1686
- // First, try to get/create with the sender's actual program ID
1664
final recipientPDA = AssociatedTokenAccountProgramUtils.associatedTokenAccount(
1665
mint: mintAddress,
1666
owner: SolAddress(destinationAddress),
1667
tokenProgramId: senderTokenProgramId,
1668
);
1669
1693
- // Check if account exists with correct program
1670
SolanaAccountInfo? recipientInfo;
1671
try {
1672
recipientInfo = await _provider!.request(
1679
recipientInfo = null;
1680
}
1681
1706
- if (recipientInfo != null && recipientInfo.owner.address == senderTokenProgramId.address) {
1707
- associatedRecipientAccount = recipientPDA;
1708
- } else {
1709
- // Create the account with the correct program ID
1710
- final createATA = AssociatedTokenAccountProgram.associatedTokenAccount(
1711
- payer: ownerPrivateKey.publicKey().toAddress(),
1712
- associatedToken: recipientPDA.address,
1713
- owner: SolAddress(destinationAddress),
1714
- mint: mintAddress,
1715
- tokenProgramId: senderTokenProgramId,
1716
- );
1717
-
1718
- final blockhash = await _getLatestBlockhash(Commitment.confirmed);
1719
- final createTransaction = SolanaTransaction(
1720
- payerKey: ownerPrivateKey.publicKey().toAddress(),
1721
- instructions: [createATA],
1722
- recentBlockhash: blockhash,
1723
- type: TransactionType.v0,
1724
- );
1725
-
1726
- final serializedCreateTx = await _signTransactionInternal(
1727
- ownerPrivateKey: ownerPrivateKey,
1728
- transaction: createTransaction,
1682
+ if (recipientInfo != null && recipientInfo.owner.address != senderTokenProgramId.address) {
1683
+ throw SolanaCreateAssociatedTokenAccountException(
1684
+ "Recipient token account is owned by ${recipientInfo.owner.address}",
1685
);
1730
-
1731
- await sendTransaction(
1732
- serializedTransaction: serializedCreateTx,
1733
- commitment: Commitment.confirmed,
1734
- );
1735
-
1736
- await Future.delayed(const Duration(seconds: 2));
1737
- associatedRecipientAccount = recipientPDA;
1686
}
1687
+
1688
+ shouldCreateRecipientAccount = recipientInfo == null;
1689
+ associatedRecipientAccount = recipientPDA;
1690
+ } on SolanaCreateAssociatedTokenAccountException {
1691
+ rethrow;
1692
} catch (e) {
1693
throw SolanaCreateAssociatedTokenAccountException(e.toString());
1694
}
1704
decimals: tokenDecimals,
1705
);
1706
1707
+ final instructions = <TransactionInstruction>[
1708
+ if (shouldCreateRecipientAccount)
1709
+ AssociatedTokenAccountProgram.associatedTokenAccountIdempotent(
1710
+ payer: ownerPrivateKey.publicKey().toAddress(),
1711
+ associatedToken: associatedRecipientAccount.address,
1712
+ owner: SolAddress(destinationAddress),
1713
+ mint: mintAddress,
1714
+ tokenProgramId: senderTokenProgramId,
1715
+ ),
1716
+ transferInstructions,
1717
+ ];
1718
+
1719
final latestBlockHash = await _getLatestBlockhash(commitment);
1720
1721
final transaction = SolanaTransaction(
1722
payerKey: ownerPrivateKey.publicKey().toAddress(),
1758
- instructions: [transferInstructions],
1723
+ instructions: instructions,
1724
recentBlockhash: latestBlockHash,
1725
);
1726
1762
- final message = await _getMessageForSPLTokenTransaction(
1763
- ownerAddress: ownerPrivateKey.publicKey().toAddress(),
1764
- tokenDecimals: tokenDecimals,
1765
- mintAddress: mintAddress,
1766
- destinationAddress: associatedRecipientAccount.address,
1767
- sourceAccount: associatedSenderAccount.address,
1768
- amount: inputAmount,
1769
- commitment: commitment,
1770
- tokenProgramId: tokenProgramId,
1727
+ final message = Message.compile(
1728
+ transactionInstructions: instructions,
1729
+ payer: ownerPrivateKey.publicKey().toAddress(),
1730
+ recentBlockhash: latestBlockHash,
1731
);
1732
1733
final fee = await _getFeeFromCompiledMessage(message, commitment);
1734
1735
+ // The sender account exists by this point, so its space is set, and the recipient
1736
+ // account is the same size because it holds the same mint under the same program.
1737
+ final accountCreationCost = shouldCreateRecipientAccount
1738
+ ? await _getRentExemptionAmount(senderAccountSpace!)
1739
+ : Money.zero(CryptoCurrency.sol);
1740
+
1741
final hasSufficientFundsLeft = await hasSufficientFundsLeftForRent(
1776
- inputAmount: Money.zero(CryptoCurrency.sol),
1777
- fee: fee,
1742
+ totalOutflow: accountCreationCost + fee,
1743
solBalance: solBalance,
1744
);
1745
1761
destinationAddress: destinationAddress,
1762
sendTransaction: sendTx,
1763
fee: fee,
1764
+ additionalCost: shouldCreateRecipientAccount ? accountCreationCost : null,
1765
);
1766
}
1767
1850
final mint = tokenData['mint'] as String? ?? '';
1851
if (mint.isEmpty) continue;
1852
1887
- final amountRaw = tokenData['amountRaw'] as String? ?? '0';
1888
-
1889
- final decimals = tokenData['decimals'] as int? ?? 0;
1890
-
1891
- final associatedTokenAddress = tokenData['associatedTokenAddress'] as String? ?? '';
1892
-
1853
tokens.add(
1854
MoralisSolanaTokenBalance(
1855
mint: mint,
1856
amount: amount,
1897
- amountRaw: amountRaw,
1898
- decimals: decimals,
1899
- associatedTokenAddress: associatedTokenAddress,
1857
),
1858
);
1859
}
1911
class MoralisSolanaTokenBalance {
1912
final String mint;
1913
final double amount;
1957
- final String amountRaw;
1958
- final int decimals;
1959
- final String associatedTokenAddress;
1914
1915
const MoralisSolanaTokenBalance({
1916
required this.mint,
1917
required this.amount,
1964
- required this.amountRaw,
1965
- required this.decimals,
1966
- required this.associatedTokenAddress,
1918
});
1919
}