What changed, and why it matters
This commit fixes how Stack Wallet sends Solana tokens. Previously, the app required the recipient to already have a token account (ATA) set up, and it could fail or send funds incorrectly if that account didn't exist. The update makes the wallet automatically create the recipient's token account during the transaction if needed, and it cleans up how token details are passed around. There is no clear security bug being patched, but the change touches code that handles real money transfers, so mistakes here could cause lost funds or failed transactions.
Treat as a functional bug fix rather than a security vulnerability. Reviewers should verify that the new ATA creation path correctly handles Token-2022 mints, that the fee estimate accounts for the extra create-account instruction, and that the null-safety assertions on pCurrentSolanaTokenWallet do not crash the UI in edge cases. No urgent security patch is indicated.
Security signals we found
Funds availability / transaction failure: prior code threw if recipient ATA did not exist, which could block legitimate sends but is not an exploit.
ATA derivation seed change: seeds now use pubkey bytes rather than a hardcoded 'account' string and base58 string conversions, fixing potential derivation mismatch.
Instruction reuse between prepareSend and confirmSend reduces risk of inconsistent transaction construction.
Removal of fallbacks to 'unknown' token metadata in UI reduces chance of user confusion, but introduces null-safety assertions (value!) that could crash if provider state is missing.
No explicit security disclosure, CVE, or researcher attribution in commit or references.
Evidence from the diff
The commit refactors Solana SPL token sending to use Associated Token Account (ATA) creation instructions. It removes token metadata fields (tokenSymbol, tokenMint, tokenDecimals, solanaRecipientTokenAccount) from TxData and instead relies on the SolanaTokenWallet provider and a new solInstructions list. prepareSend now checks whether the recipient ATA exists on-chain; if not, it adds an AssociatedTokenAccountInstruction.createAccount instruction before the TransferChecked instruction. confirmSend now reuses the prepared instructions rather than rebuilding them. The ATA derivation was also corrected to use raw public key bytes as seeds instead of a UTF-8 ‘account’ string and base58-encoded strings. The token program detection was simplified to compare against Token2022Program.programId.
Changed components
lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dartlib/wallets/models/tx_data.dartlib/pages/send_view/confirm_transaction_view.dartlib/pages/send_view/sol_token_send_view.dartlib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dartInspect captured patch +168 / −299
diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart
index 9538719..fe6525a 100644
--- a/lib/pages/send_view/confirm_transaction_view.dart
+++ b/lib/pages/send_view/confirm_transaction_view.dart
@@ -17,7 +17,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
-import '../../models/isar/models/solana/sol_contract.dart';
import '../../models/isar/models/transaction_note.dart';
import '../../notifications/show_flush_bar.dart';
import '../../pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart';
@@ -557,8 +556,8 @@ class _ConfirmTransactionViewState
if (wallet is SolanaWallet) {
// For Solana tokens, use the Solana token wallet provider or TxData as fallback.
unit = ref.watch(
- pCurrentSolanaTokenWallet.select((value) => value?.tokenSymbol),
- ) ?? widget.txData.tokenSymbol ?? "TOKEN";
+ pCurrentSolanaTokenWallet.select((value) => value!.tokenSymbol),
+ );
} else {
// For Ethereum tokens, use the Ethereum token wallet provider.
unit = ref.watch(
@@ -724,18 +723,17 @@ class _ConfirmTransactionViewState
.watch(pAmountFormatter(coin))
.format(
amountWithoutChange,
- ethContract: widget.isTokenTx && wallet is! SolanaWallet
+ ethContract:
+ widget.isTokenTx && wallet is! SolanaWallet
? ref
.watch(pCurrentTokenWallet)!
.tokenContract
: null,
- solContract: widget.isTokenTx && wallet is SolanaWallet
- ? SolContract(
- address: widget.txData.tokenMint ?? "unknown",
- name: widget.txData.tokenSymbol ?? "Token",
- symbol: widget.txData.tokenSymbol ?? "TOKEN",
- decimals: widget.txData.tokenDecimals ?? 9,
- )
+ solContract:
+ widget.isTokenTx && wallet is SolanaWallet
+ ? ref
+ .watch(pCurrentSolanaTokenWallet)!
+ .solContract
: null,
),
style: STextStyles.itemSubtitle12(context),
@@ -923,33 +921,33 @@ class _ConfirmTransactionViewState
if (externalCalls) {
final price = widget.isTokenTx
? (wallet is SolanaWallet
- ? // For Solana tokens, use tokenMint from provider or TxData.
- ref
- .read(
- priceAnd24hChangeNotifierProvider,
- )
- .getTokenPrice(
- ref
+ ? // For Solana tokens, use tokenMint from provider or TxData.
+ ref
+ .read(
+ priceAnd24hChangeNotifierProvider,
+ )
+ .getTokenPrice(
+ ref
.read(
pCurrentSolanaTokenWallet,
- )
- ?.tokenMint ??
- widget.txData.tokenMint ??
- "unknown",
- )
- ?.value
- : // For Ethereum tokens, use contract address.
- ref
- .read(
- priceAnd24hChangeNotifierProvider,
- )
- .getTokenPrice(
- ref
- .read(pCurrentTokenWallet)!
- .tokenContract
- .address,
- )
- ?.value)
+ )!
+ .tokenMint,
+ )
+ ?.value
+ : // For Ethereum tokens, use contract address.
+ ref
+ .read(
+ priceAnd24hChangeNotifierProvider,
+ )
+ .getTokenPrice(
+ ref
+ .read(
+ pCurrentTokenWallet,
+ )!
+ .tokenContract
+ .address,
+ )
+ ?.value)
: ref
.read(
priceAnd24hChangeNotifierProvider,
@@ -977,20 +975,23 @@ class _ConfirmTransactionViewState
.watch(pAmountFormatter(coin))
.format(
amountWithoutChange,
- ethContract: widget.isTokenTx && wallet is! SolanaWallet
+ ethContract:
+ widget.isTokenTx &&
+ wallet is! SolanaWallet
? ref
.watch(
pCurrentTokenWallet,
)!
.tokenContract
: null,
- solContract: widget.isTokenTx && wallet is SolanaWallet
- ? SolContract(
- address: widget.txData.tokenMint ?? "unknown",
- name: widget.txData.tokenSymbol ?? "Token",
- symbol: widget.txData.tokenSymbol ?? "TOKEN",
- decimals: widget.txData.tokenDecimals ?? 9,
- )
+ solContract:
+ widget.isTokenTx &&
+ wallet is SolanaWallet
+ ? ref
+ .watch(
+ pCurrentSolanaTokenWallet,
+ )!
+ .solContract
: null,
),
style:
diff --git a/lib/pages/send_view/sol_token_send_view.dart b/lib/pages/send_view/sol_token_send_view.dart
index d35d177..6187d4c 100644
--- a/lib/pages/send_view/sol_token_send_view.dart
+++ b/lib/pages/send_view/sol_token_send_view.dart
@@ -472,9 +472,6 @@ class _SolTokenSendViewState extends ConsumerState<SolTokenSendView> {
memo: memoController.text.isEmpty ? null : memoController.text,
feeRateType: ref.read(feeRateTypeMobileStateProvider),
note: noteController.text,
- tokenMint: tokenMint,
- tokenSymbol: tokenWallet.tokenSymbol,
- tokenDecimals: tokenWallet.tokenDecimals,
),
);
diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart
index 7dd0c81..cd4e227 100644
--- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart
+++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_sol_token_send.dart
@@ -261,9 +261,6 @@ class _DesktopSolTokenSendState extends ConsumerState<DesktopSolTokenSend> {
),
],
memo: memo,
- tokenSymbol: tokenSymbol,
- tokenMint: tokenMint,
- tokenDecimals: tokenDecimals,
),
);
@@ -272,12 +269,7 @@ class _DesktopSolTokenSendState extends ConsumerState<DesktopSolTokenSend> {
txData = results.first as TxData;
if (!wasCancelled && mounted) {
- txData = txData.copyWith(
- note: _note ?? "",
- tokenSymbol: tokenSymbol,
- tokenMint: tokenMint,
- tokenDecimals: tokenDecimals,
- );
+ txData = txData.copyWith(note: _note ?? "");
// pop building dialog
Navigator.of(context, rootNavigator: true).pop();
diff --git a/lib/wallets/models/tx_data.dart b/lib/wallets/models/tx_data.dart
index 0ac1d29..14c7186 100644
--- a/lib/wallets/models/tx_data.dart
+++ b/lib/wallets/models/tx_data.dart
@@ -1,5 +1,6 @@
import 'dart:typed_data';
+import 'package:solana/encoder.dart' show Instruction;
import 'package:tezart/tezart.dart' as tezart;
import 'package:web3dart/web3dart.dart' as web3dart;
@@ -70,11 +71,8 @@ class TxData {
final int? nonce;
final BigInt? chainId;
- // Solana & Ethereum token-specific.
- final String? tokenSymbol;
- final String? tokenMint;
- final int? tokenDecimals;
- final String? solanaRecipientTokenAccount;
+ // Solana token-specific.
+ final List<Instruction>? solInstructions;
// wownero and monero specific
final CsPendingTransaction? pendingTransaction;
@@ -137,10 +135,7 @@ class TxData {
this.web3dartTransaction,
this.nonce,
this.chainId,
- this.tokenSymbol,
- this.tokenMint,
- this.tokenDecimals,
- this.solanaRecipientTokenAccount,
+ this.solInstructions,
this.pendingTransaction,
this.pendingSalviumTransaction,
this.tezosOperationsList,
@@ -279,10 +274,7 @@ class TxData {
web3dart.Transaction? web3dartTransaction,
int? nonce,
BigInt? chainId,
- String? tokenSymbol,
- String? tokenMint,
- int? tokenDecimals,
- String? solanaRecipientTokenAccount,
+ List<Instruction>? solInstructions,
CsPendingTransaction? pendingTransaction,
CsPendingTransaction? pendingSalviumTransaction,
int? jMintValue,
@@ -334,11 +326,7 @@ class TxData {
web3dartTransaction: web3dartTransaction ?? this.web3dartTransaction,
nonce: nonce ?? this.nonce,
chainId: chainId ?? this.chainId,
- tokenSymbol: tokenSymbol ?? this.tokenSymbol,
- tokenMint: tokenMint ?? this.tokenMint,
- tokenDecimals: tokenDecimals ?? this.tokenDecimals,
- solanaRecipientTokenAccount:
- solanaRecipientTokenAccount ?? this.solanaRecipientTokenAccount,
+ solInstructions: solInstructions ?? this.solInstructions,
pendingTransaction: pendingTransaction ?? this.pendingTransaction,
pendingSalviumTransaction:
pendingSalviumTransaction ?? this.pendingSalviumTransaction,
@@ -381,6 +369,7 @@ class TxData {
'web3dartTransaction: $web3dartTransaction, '
'nonce: $nonce, '
'chainId: $chainId, '
+ 'solInstructions: $solInstructions, '
'pendingTransaction: $pendingTransaction, '
'pendingSalviumTransaction: $pendingSalviumTransaction, '
'tezosOperationsList: $tezosOperationsList, '
diff --git a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
index 65fbe32..49879a1 100644
--- a/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
+++ b/lib/wallets/wallet/impl/sub_wallets/solana_token_wallet.dart
@@ -10,7 +10,8 @@
import 'dart:convert';
import 'package:isar_community/isar.dart';
-import 'package:solana/dto.dart';
+import 'package:solana/dto.dart' hide Instruction;
+import 'package:solana/encoder.dart' show Instruction;
import 'package:solana/solana.dart' hide Wallet;
import '../../../../models/balance.dart';
@@ -21,7 +22,6 @@ import '../../../../models/isar/models/isar_models.dart';
import '../../../../models/paymint/fee_object_model.dart';
import '../../../../services/solana/solana_token_api.dart';
import '../../../../utilities/amount/amount.dart';
-import '../../../../utilities/extensions/extensions.dart';
import '../../../../utilities/logger.dart';
import '../../../models/tx_data.dart';
import '../../wallet.dart';
@@ -140,7 +140,7 @@ class SolanaTokenWallet extends Wallet {
rpcClient: rpcClient,
);
- if (recipientTokenAccount == null || recipientTokenAccount.isEmpty) {
+ if (recipientTokenAccount.isEmpty) {
throw Exception(
"Cannot determine recipient token account for mint $tokenMint. "
"Recipient may not have a token account for this mint. "
@@ -148,39 +148,6 @@ class SolanaTokenWallet extends Wallet {
);
}
- try {
- final recipientAccountInfo = await rpcClient.getAccountInfo(
- recipientTokenAccount,
- encoding: Encoding.jsonParsed,
- );
- if (recipientAccountInfo.value == null) {
- throw Exception(
- "Recipient token account $recipientTokenAccount does not exist on-chain. "
- "The recipient must initialize their token account before receiving tokens. "
- "You can ask the recipient to accept the token in their wallet app first.",
- );
- }
-
- final accountData = recipientAccountInfo.value!;
-
- // Verify account is owned by token program (not System Program).
- if (accountData.owner == '11111111111111111111111111111111') {
- throw Exception(
- "Recipient token account $recipientTokenAccount is owned by the System Program, "
- "not a token program. The account may not be a valid token account.",
- );
- }
- } catch (e) {
- if (e.toString().contains("does not exist") ||
- e.toString().contains("not owned by")) {
- rethrow;
- }
- throw Exception(
- "Failed to validate recipient token account: $e. "
- "Ensure the recipient has initialized their token account.",
- );
- }
-
final senderTokenAccountKey = Ed25519HDPublicKey.fromBase58(
senderTokenAccount,
);
@@ -216,12 +183,47 @@ class SolanaTokenWallet extends Wallet {
}
final TokenProgramType tokenProgram =
- tokenProgramId != 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA' &&
- tokenProgramId.startsWith('Token')
+ tokenProgramId == Token2022Program.programId
? TokenProgramType.token2022Program
: TokenProgramType.tokenProgram;
- // ignore: unused_local_variable
+ final recipientAccountInfo = await rpcClient.getAccountInfo(
+ recipientTokenAccount,
+ encoding: Encoding.jsonParsed,
+ );
+
+ AssociatedTokenAccountInstruction? createAccountInstruction;
+ if (recipientAccountInfo.value == null) {
+ createAccountInstruction =
+ AssociatedTokenAccountInstruction.createAccount(
+ funder: keyPair.publicKey,
+ address: recipientTokenAccountKey,
+ owner: Ed25519HDPublicKey.fromBase58(recipientAddress),
+ mint: mintPubkey,
+ );
+ } else {
+ try {
+ final accountData = recipientAccountInfo.value!;
+
+ // Verify account is owned by token program (not System Program).
+ if (accountData.owner == '11111111111111111111111111111111') {
+ throw Exception(
+ "Recipient token account $recipientTokenAccount is owned by the System Program, "
+ "not a token program. The account may not be a valid token account.",
+ );
+ }
+ } catch (e) {
+ if (e.toString().contains("does not exist") ||
+ e.toString().contains("not owned by")) {
+ rethrow;
+ }
+ throw Exception(
+ "Failed to validate recipient token account: $e. "
+ "Ensure the recipient has initialized their token account.",
+ );
+ }
+ }
+
final instruction = TokenInstruction.transferChecked(
source: senderTokenAccountKey,
destination: recipientTokenAccountKey,
@@ -232,20 +234,23 @@ class SolanaTokenWallet extends Wallet {
tokenProgram: tokenProgram,
);
+ final instructions = [
+ if (createAccountInstruction != null) createAccountInstruction,
+ instruction,
+ ];
+
final feeEstimate =
await _getEstimatedTokenTransferFee(
- senderTokenAccountKey: senderTokenAccountKey,
- recipientTokenAccountKey: recipientTokenAccountKey,
ownerPublicKey: keyPair.publicKey,
- amount: txData.amount!.raw.toInt(),
rpcClient: rpcClient,
+ instructions: instructions,
memo: txData.memo,
) ??
5000;
return txData.copyWith(
fee: Amount(rawValue: BigInt.from(feeEstimate), fractionDigits: 9),
- solanaRecipientTokenAccount: recipientTokenAccount,
+ solInstructions: instructions,
);
} catch (e, s) {
Logging.instance.e(
@@ -291,67 +296,19 @@ class SolanaTokenWallet extends Wallet {
await rpcClient.getLatestBlockhash();
- // Reuse the recipient token account from prepareSend (already looked up once).
- final recipientTokenAccount = txData.solanaRecipientTokenAccount;
+ final instructions = txData.solInstructions;
- if (recipientTokenAccount == null || recipientTokenAccount.isEmpty) {
+ if (instructions == null || instructions.isEmpty) {
throw Exception(
- "Recipient token account not found in prepared transaction. "
- "Call prepareSend() first to determine the recipient's token account.",
+ "Token transaction missing instructions. "
+ "Call prepareSend() first.",
);
}
- // Build SPL token tx instruction.
- final senderTokenAccountKey = Ed25519HDPublicKey.fromBase58(
- senderTokenAccount,
- );
- final recipientTokenAccountKey = Ed25519HDPublicKey.fromBase58(
- recipientTokenAccount,
- );
- final mintPubkey = Ed25519HDPublicKey.fromBase58(tokenMint);
-
- // Query the actual token program owner (important for Token-2022 variants).
- String tokenProgramId;
- try {
- final mintInfo = await rpcClient.getAccountInfo(
- tokenMint,
- encoding: Encoding.jsonParsed,
- );
- if (mintInfo.value != null) {
- tokenProgramId = mintInfo.value!.owner;
- Logging.instance.i(
- "$runtimeType confirmSend: Token program owner = $tokenProgramId for mint $tokenMint",
- );
- } else {
- // Fallback to SPL Token.
- tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
- Logging.instance.w(
- "$runtimeType confirmSend: Could not query mint owner, using SPL Token",
- );
- }
- } catch (e) {
- // Fallback to SPL Token on error.
- tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
- Logging.instance.w(
- "$runtimeType confirmSend: Error querying mint owner: $e, using SPL Token",
- );
- }
-
- // Build the TransferChecked instruction.
- final TokenProgramType tokenProgram =
- tokenProgramId != 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA' &&
- tokenProgramId.startsWith('Token') // Token-2022 variant.
- ? TokenProgramType.token2022Program
- : TokenProgramType.tokenProgram;
-
- final instruction = TokenInstruction.transferChecked(
- source: senderTokenAccountKey,
- destination: recipientTokenAccountKey,
- mint: mintPubkey,
- owner: keyPair.publicKey,
- decimals: tokenDecimals,
- amount: txData.amount!.raw.toInt(),
- tokenProgram: tokenProgram,
+ final recipientTokenAccount = await _findOrDeriveRecipientTokenAccount(
+ recipientAddress: txData.recipients!.first.address,
+ mint: tokenMint,
+ rpcClient: rpcClient,
);
// Create message.
@@ -359,7 +316,7 @@ class SolanaTokenWallet extends Wallet {
instructions: [
if (txData.memo != null)
MemoInstruction(signers: const [], memo: txData.memo!),
- instruction,
+ ...instructions,
],
);
@@ -785,170 +742,103 @@ class SolanaTokenWallet extends Wallet {
}
}
- Future<String?> _findOrDeriveRecipientTokenAccount({
+ Future<String> _findOrDeriveRecipientTokenAccount({
required String recipientAddress,
required String mint,
required RpcClient rpcClient,
}) async {
- try {
- // First, try to find an existing token account
- final existingAccount = await _findTokenAccount(
- ownerAddress: recipientAddress,
- mint: mint,
- rpcClient: rpcClient,
- );
-
- if (existingAccount != null) {
- Logging.instance.i(
- "$runtimeType Found existing token account for recipient: $existingAccount",
- );
- return existingAccount;
- }
-
- // If no existing account found, try to derive the ATA
+ // First, try to find an existing token account
+ final existingAccount = await _findTokenAccount(
+ ownerAddress: recipientAddress,
+ mint: mint,
+ rpcClient: rpcClient,
+ );
+
+ if (existingAccount != null) {
Logging.instance.i(
- "$runtimeType No existing token account found, deriving ATA for recipient",
+ "$runtimeType Found existing token account for recipient: $existingAccount",
);
-
- try {
- final ataAddress = await _deriveAtaAddress(
- ownerAddress: recipientAddress,
- mint: mint,
- rpcClient: rpcClient,
- );
- if (ataAddress != null) {
- Logging.instance.i("$runtimeType Derived ATA address: $ataAddress");
- return ataAddress;
- } else {
- Logging.instance.w("$runtimeType ATA derivation returned null");
- return null;
- }
- } catch (derivationError) {
- Logging.instance.w(
- "$runtimeType Failed to derive ATA address: $derivationError",
- );
- return null;
- }
- } catch (e) {
- Logging.instance.w(
- "$runtimeType _findOrDeriveRecipientTokenAccount error: $e",
- );
- return null;
+ return existingAccount;
}
+
+ // If no existing account found, try to derive the ATA
+ Logging.instance.i(
+ "$runtimeType No existing token account found, deriving ATA for recipient",
+ );
+
+ return await _deriveAtaAddress(
+ ownerAddress: recipientAddress,
+ mint: mint,
+ rpcClient: rpcClient,
+ );
}
- Future<String?> _deriveAtaAddress({
+ Future<String> _deriveAtaAddress({
required String ownerAddress,
required String mint,
required RpcClient rpcClient,
}) async {
- try {
- final ownerPubkey = Ed25519HDPublicKey.fromBase58(ownerAddress);
- final mintPubkey = Ed25519HDPublicKey.fromBase58(mint);
+ final ownerPubkey = Ed25519HDPublicKey.fromBase58(ownerAddress);
+ final mintPubkey = Ed25519HDPublicKey.fromBase58(mint);
- final tokenApi = SolanaTokenAPI();
- tokenApi.initializeRpcClient(rpcClient);
+ final tokenApi = SolanaTokenAPI();
+ tokenApi.initializeRpcClient(rpcClient);
- String tokenProgramId;
- try {
- final mintInfo = await rpcClient.getAccountInfo(
- mint,
- encoding: Encoding.jsonParsed,
- );
- if (mintInfo.value != null) {
- tokenProgramId = mintInfo.value!.owner;
- } else {
- tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
- }
- } catch (e) {
+ String tokenProgramId;
+ try {
+ final mintInfo = await rpcClient.getAccountInfo(
+ mint,
+ encoding: Encoding.jsonParsed,
+ );
+ if (mintInfo.value != null) {
+ tokenProgramId = mintInfo.value!.owner;
+ } else {
tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
}
+ } catch (e) {
+ tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
+ }
- final tokenProgramPubkey = Ed25519HDPublicKey.fromBase58(tokenProgramId);
+ final tokenProgramPubkey = Ed25519HDPublicKey.fromBase58(tokenProgramId);
- const associatedTokenProgramId =
- 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
- final associatedTokenProgramPubkey = Ed25519HDPublicKey.fromBase58(
- associatedTokenProgramId,
- );
+ const associatedTokenProgramId =
+ 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
+ final associatedTokenProgramPubkey = Ed25519HDPublicKey.fromBase58(
+ associatedTokenProgramId,
+ );
- final seeds = [
- 'account'.toUint8ListFromUtf8,
- ownerPubkey.toBase58().toUint8ListFromBase58Encoded,
- tokenProgramPubkey.toBase58().toUint8ListFromBase58Encoded,
- mintPubkey.toBase58().toUint8ListFromBase58Encoded,
- ];
+ final seeds = [
+ ownerPubkey.bytes,
+ tokenProgramPubkey.bytes,
+ mintPubkey.bytes,
+ ];
- final ataAddress = await Ed25519HDPublicKey.findProgramAddress(
- seeds: seeds,
- programId: associatedTokenProgramPubkey,
- );
+ final ataAddress = await Ed25519HDPublicKey.findProgramAddress(
+ seeds: seeds,
+ programId: associatedTokenProgramPubkey,
+ );
- final ataBase58 = ataAddress.toBase58();
+ final ataBase58 = ataAddress.toBase58();
- return ataBase58;
- } catch (e, stackTrace) {
- Logging.instance.w(
- "$runtimeType _deriveAtaAddress error: $e",
- error: e,
- stackTrace: stackTrace,
- );
- return null;
- }
+ return ataBase58;
}
Future<int?> _getEstimatedTokenTransferFee({
- required Ed25519HDPublicKey senderTokenAccountKey,
- required Ed25519HDPublicKey recipientTokenAccountKey,
required Ed25519HDPublicKey ownerPublicKey,
- required int amount,
required RpcClient rpcClient,
+ required List<Instruction> instructions,
required String? memo,
}) async {
try {
// Get latest blockhash for message compilation.
final latestBlockhash = await rpcClient.getLatestBlockhash();
- final mintPubkey = Ed25519HDPublicKey.fromBase58(tokenMint);
-
- // Query the actual token program owner (important for Token-2022 variants).
- String tokenProgramId;
- try {
- final mintInfo = await rpcClient.getAccountInfo(
- tokenMint,
- encoding: Encoding.jsonParsed,
- );
- tokenProgramId =
- mintInfo.value?.owner ??
- 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
- } catch (e) {
- tokenProgramId = 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA';
- }
-
- // Build the TransferChecked instruction.
- // Determine which token program type to use based on the queried owner.
- final TokenProgramType tokenProgram =
- tokenProgramId != 'TokenkegQfeZyiNwAJsyFbPVwwQQfg5bgUiqhStM5QA' &&
- tokenProgramId.startsWith('Token')
- ? TokenProgramType.token2022Program
- : TokenProgramType.tokenProgram;
-
- final instruction = TokenInstruction.transferChecked(
- source: senderTokenAccountKey,
- destination: recipientTokenAccountKey,
- mint: mintPubkey,
- owner: ownerPublicKey,
- decimals: tokenDecimals,
- amount: amount,
- tokenProgram: tokenProgram,
- );
-
// Compile the message with the blockhash.
final compiledMessage =
Message(
instructions: [
if (memo != null) MemoInstruction(signers: const [], memo: memo),
- instruction,
+ ...instructions,
],
).compile(
recentBlockhash: latestBlockhash.value.blockhash,
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.