mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
CW-1084: Solana Issues (#2305)
* fix(solana-issues): Fix missing solana transaction history entries * fix(solana-issues): Fixes issues relating to Solana Transaction History This change: - Modifies transaction parsing logic to handle more scenarios and better parse Solana transaction data. - Adds partial filter for spam transactions * fix(solana-issues): Enhance transaction parsing for Associated Token Account (ATA) programs This change: - Adds logic to differentiate between create account and token transfer transactions for the ATA program. - Introduces a check to skip transactions that only create accounts without associated token transfers. * fix(solana-issues): Improve transaction update logic and enhance error handling This change: - Updates the transaction update callback to only trigger when new valid transactions are present. - Enhances error handling for insufficient funds by distinguishing between errors for sender and receiver.
This commit is contained in:
parent
e5d0194f11
commit
fe0c9ecc0e
30 changed files with 263 additions and 157 deletions
|
@ -13,13 +13,17 @@ import 'package:cw_solana/solana_transaction_model.dart';
|
|||
import 'package:cw_solana/spl_token.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:on_chain/solana/solana.dart';
|
||||
import 'package:on_chain/solana/src/instructions/associated_token_account/constant.dart';
|
||||
import 'package:on_chain/solana/src/models/pda/pda.dart';
|
||||
import 'package:blockchain_utils/blockchain_utils.dart';
|
||||
import 'package:on_chain/solana/src/rpc/models/models/confirmed_transaction_meta.dart';
|
||||
import '.secrets.g.dart' as secrets;
|
||||
|
||||
class SolanaWalletClient {
|
||||
final httpClient = http.Client();
|
||||
SolanaRPC? _provider;
|
||||
// Minimum amount in SOL to consider a transaction valid (to filter spam)
|
||||
static const double minValidAmount = 0.00000003;
|
||||
|
||||
bool connect(Node node) {
|
||||
try {
|
||||
|
@ -155,170 +159,88 @@ class SolanaWalletClient {
|
|||
if (meta == null || transaction == null) return null;
|
||||
|
||||
final int fee = meta.fee;
|
||||
final feeInSol = fee / SolanaUtils.lamportsPerSol;
|
||||
|
||||
final message = transaction.message;
|
||||
final instructions = message.compiledInstructions;
|
||||
|
||||
String sender = "";
|
||||
String receiver = "";
|
||||
|
||||
String signature = (txResponse.transaction?.signatures.isEmpty ?? true)
|
||||
? ""
|
||||
: Base58Encoder.encode(txResponse.transaction!.signatures.first);
|
||||
|
||||
|
||||
for (final instruction in instructions) {
|
||||
final programId = message.accountKeys[instruction.programIdIndex];
|
||||
|
||||
if (programId == SystemProgramConst.programId) {
|
||||
if (programId == SystemProgramConst.programId ||
|
||||
programId == ComputeBudgetConst.programId) {
|
||||
// For native solana transactions
|
||||
if (instruction.accounts.length < 2) continue;
|
||||
|
||||
if (txResponse.version == TransactionType.legacy) {
|
||||
// For legacy transfers, the fee payer (index 0) is the sender.
|
||||
sender = message.accountKeys[0].address;
|
||||
// Get the fee payer index based on transaction type
|
||||
// For legacy transfers, the first account is usually the fee payer
|
||||
// For versioned, the first account in instruction is usually the fee payer
|
||||
final feePayerIndex =
|
||||
txResponse.version == TransactionType.legacy ? 0 : instruction.accounts[0];
|
||||
|
||||
final senderPreBalance = meta.preBalances[0];
|
||||
final senderPostBalance = meta.postBalances[0];
|
||||
final feeForTx = fee / SolanaUtils.lamportsPerSol;
|
||||
final transactionModel = await _parseNativeTransaction(
|
||||
message: message,
|
||||
meta: meta,
|
||||
fee: fee,
|
||||
feeInSol: feeInSol,
|
||||
feePayerIndex: feePayerIndex,
|
||||
walletAddress: walletAddress,
|
||||
signature: signature,
|
||||
blockTime: blockTime,
|
||||
);
|
||||
|
||||
// The loss on the sender's account would include both the transfer amount and the fee.
|
||||
// So we would subtract the fee to calculate the actual amount that was transferred (in lamports).
|
||||
final transferLamports = (senderPreBalance - senderPostBalance) - BigInt.from(fee);
|
||||
|
||||
// Next, we attempt to find the receiver by comparing the balance changes.
|
||||
// (The index 0 is for the sender so we skip it.)
|
||||
bool foundReceiver = false;
|
||||
for (int i = 1; i < meta.preBalances.length; i++) {
|
||||
// The increase in balance on the receiver account should correspond to the transfer amount we calculated earlieer.
|
||||
final pre = meta.preBalances[i];
|
||||
final post = meta.postBalances[i];
|
||||
if ((post - pre) == transferLamports) {
|
||||
receiver = message.accountKeys[i].address;
|
||||
foundReceiver = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundReceiver) {
|
||||
// Optionally (and rarely), if no account shows the exact expected change,
|
||||
// we set the receiver address to unknown.
|
||||
receiver = "unknown";
|
||||
}
|
||||
|
||||
final amount = transferLamports / BigInt.from(1e9);
|
||||
|
||||
return SolanaTransactionModel(
|
||||
isOutgoingTx: sender == walletAddress,
|
||||
from: sender,
|
||||
to: receiver,
|
||||
id: signature,
|
||||
amount: amount.abs(),
|
||||
programId: SystemProgramConst.programId.address,
|
||||
tokenSymbol: 'SOL',
|
||||
blockTimeInInt: blockTime?.toInt() ?? 0,
|
||||
fee: feeForTx,
|
||||
);
|
||||
} else {
|
||||
if (instruction.accounts.length < 2) continue;
|
||||
final senderIndex = instruction.accounts[0];
|
||||
final receiverIndex = instruction.accounts[1];
|
||||
|
||||
sender = message.accountKeys[senderIndex].address;
|
||||
receiver = message.accountKeys[receiverIndex].address;
|
||||
|
||||
final feeForTx = fee / SolanaUtils.lamportsPerSol;
|
||||
|
||||
final preBalances = meta.preBalances;
|
||||
final postBalances = meta.postBalances;
|
||||
|
||||
final amountInString =
|
||||
(((preBalances[senderIndex] - postBalances[senderIndex]) / BigInt.from(1e9))
|
||||
.toDouble() -
|
||||
feeForTx)
|
||||
.toStringAsFixed(6);
|
||||
|
||||
final amount = double.parse(amountInString);
|
||||
|
||||
return SolanaTransactionModel(
|
||||
isOutgoingTx: sender == walletAddress,
|
||||
from: sender,
|
||||
to: receiver,
|
||||
id: signature,
|
||||
amount: amount.abs(),
|
||||
programId: SystemProgramConst.programId.address,
|
||||
tokenSymbol: 'SOL',
|
||||
blockTimeInInt: blockTime?.toInt() ?? 0,
|
||||
fee: feeForTx,
|
||||
);
|
||||
if (transactionModel != null) {
|
||||
return transactionModel;
|
||||
}
|
||||
} else if (programId == SPLTokenProgramConst.tokenProgramId) {
|
||||
// For SPL Token transactions
|
||||
if (instruction.accounts.length < 2) continue;
|
||||
|
||||
final preBalances = meta.preTokenBalances;
|
||||
final postBalances = meta.postTokenBalances;
|
||||
|
||||
double amount = 0.0;
|
||||
bool isOutgoing = false;
|
||||
String? mintAddress;
|
||||
|
||||
double userPreAmount = 0.0;
|
||||
if (preBalances != null && preBalances.isNotEmpty) {
|
||||
for (final preBal in preBalances) {
|
||||
if (preBal.owner?.address == walletAddress) {
|
||||
userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
|
||||
|
||||
mintAddress = preBal.mint.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double userPostAmount = 0.0;
|
||||
if (postBalances != null && postBalances.isNotEmpty) {
|
||||
for (final postBal in postBalances) {
|
||||
if (postBal.owner?.address == walletAddress) {
|
||||
userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
|
||||
|
||||
mintAddress ??= postBal.mint.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final diff = userPreAmount - userPostAmount;
|
||||
final rawAmount = diff.abs();
|
||||
|
||||
final amountInString = rawAmount.toStringAsFixed(6);
|
||||
amount = double.parse(amountInString);
|
||||
|
||||
isOutgoing = diff > 0;
|
||||
|
||||
if (mintAddress == null && instruction.accounts.length >= 4) {
|
||||
final mintIndex = instruction.accounts[3];
|
||||
mintAddress = message.accountKeys[mintIndex].address;
|
||||
}
|
||||
|
||||
final sender = message.accountKeys[instruction.accounts[0]].address;
|
||||
final receiver = message.accountKeys[instruction.accounts[1]].address;
|
||||
|
||||
String? tokenSymbol = splTokenSymbol;
|
||||
|
||||
if (tokenSymbol == null && mintAddress != null) {
|
||||
final token = await getTokenInfo(mintAddress);
|
||||
tokenSymbol = token?.symbol;
|
||||
}
|
||||
|
||||
return SolanaTransactionModel(
|
||||
isOutgoingTx: isOutgoing,
|
||||
from: sender,
|
||||
to: receiver,
|
||||
id: signature,
|
||||
amount: amount,
|
||||
programId: SPLTokenProgramConst.tokenProgramId.address,
|
||||
blockTimeInInt: blockTime?.toInt() ?? 0,
|
||||
tokenSymbol: tokenSymbol ?? '',
|
||||
fee: fee / SolanaUtils.lamportsPerSol,
|
||||
final transactionModel = await _parseSPLTokenTransaction(
|
||||
message: message,
|
||||
meta: meta,
|
||||
fee: fee,
|
||||
feeInSol: feeInSol,
|
||||
instruction: instruction,
|
||||
walletAddress: walletAddress,
|
||||
signature: signature,
|
||||
blockTime: blockTime,
|
||||
splTokenSymbol: splTokenSymbol,
|
||||
);
|
||||
|
||||
if (transactionModel != null) {
|
||||
return transactionModel;
|
||||
}
|
||||
} else if (programId == AssociatedTokenAccountProgramConst.associatedTokenProgramId) {
|
||||
// For ATA program, we need to check if this is a create account transaction
|
||||
// or if it's part of a normal token transfer
|
||||
|
||||
// We skip this transaction if this is the only instruction (this means that it's a create account transaction)
|
||||
if (instructions.length == 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We look for a token transfer instruction in the same transaction
|
||||
bool hasTokenTransfer = false;
|
||||
for (final otherInstruction in instructions) {
|
||||
final otherProgramId = message.accountKeys[otherInstruction.programIdIndex];
|
||||
if (otherProgramId == SPLTokenProgramConst.tokenProgramId) {
|
||||
hasTokenTransfer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no token transfer instruction, it means this is just an ATA creation transaction
|
||||
if (!hasTokenTransfer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -330,6 +252,144 @@ class SolanaWalletClient {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<SolanaTransactionModel?> _parseNativeTransaction({
|
||||
required VersionedMessage message,
|
||||
required ConfirmedTransactionMeta meta,
|
||||
required int fee,
|
||||
required double feeInSol,
|
||||
required int feePayerIndex,
|
||||
required String walletAddress,
|
||||
required String signature,
|
||||
required BigInt? blockTime,
|
||||
}) async {
|
||||
// Calculate total balance changes across all accounts
|
||||
BigInt totalBalanceChange = BigInt.zero;
|
||||
String? sender;
|
||||
String? receiver;
|
||||
|
||||
for (int i = 0; i < meta.preBalances.length; i++) {
|
||||
final preBalance = meta.preBalances[i];
|
||||
final postBalance = meta.postBalances[i];
|
||||
final balanceChange = preBalance - postBalance;
|
||||
|
||||
if (balanceChange > BigInt.zero) {
|
||||
// This account sent funds
|
||||
sender = message.accountKeys[i].address;
|
||||
totalBalanceChange += balanceChange;
|
||||
} else if (balanceChange < BigInt.zero) {
|
||||
// This account received funds
|
||||
receiver = message.accountKeys[i].address;
|
||||
}
|
||||
}
|
||||
|
||||
// We subtract the fee from total balance change if the fee payer is the sender
|
||||
if (sender == message.accountKeys[feePayerIndex].address) {
|
||||
totalBalanceChange -= BigInt.from(fee);
|
||||
}
|
||||
|
||||
if (sender == null || receiver == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final amount = totalBalanceChange / BigInt.from(1e9);
|
||||
final amountInSol = amount.abs().toDouble();
|
||||
|
||||
// Skip transactions with very small amounts (likely spam)
|
||||
if (amountInSol < minValidAmount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SolanaTransactionModel(
|
||||
isOutgoingTx: sender == walletAddress,
|
||||
from: sender,
|
||||
to: receiver,
|
||||
id: signature,
|
||||
amount: amountInSol,
|
||||
programId: SystemProgramConst.programId.address,
|
||||
tokenSymbol: 'SOL',
|
||||
blockTimeInInt: blockTime?.toInt() ?? 0,
|
||||
fee: feeInSol,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SolanaTransactionModel?> _parseSPLTokenTransaction({
|
||||
required VersionedMessage message,
|
||||
required ConfirmedTransactionMeta meta,
|
||||
required int fee,
|
||||
required double feeInSol,
|
||||
required CompiledInstruction instruction,
|
||||
required String walletAddress,
|
||||
required String signature,
|
||||
required BigInt? blockTime,
|
||||
String? splTokenSymbol,
|
||||
}) async {
|
||||
final preBalances = meta.preTokenBalances;
|
||||
final postBalances = meta.postTokenBalances;
|
||||
|
||||
double amount = 0.0;
|
||||
bool isOutgoing = false;
|
||||
String? mintAddress;
|
||||
|
||||
double userPreAmount = 0.0;
|
||||
if (preBalances != null && preBalances.isNotEmpty) {
|
||||
for (final preBal in preBalances) {
|
||||
if (preBal.owner?.address == walletAddress) {
|
||||
userPreAmount = preBal.uiTokenAmount.uiAmount ?? 0.0;
|
||||
|
||||
mintAddress = preBal.mint.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double userPostAmount = 0.0;
|
||||
if (postBalances != null && postBalances.isNotEmpty) {
|
||||
for (final postBal in postBalances) {
|
||||
if (postBal.owner?.address == walletAddress) {
|
||||
userPostAmount = postBal.uiTokenAmount.uiAmount ?? 0.0;
|
||||
|
||||
mintAddress ??= postBal.mint.address;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final diff = userPreAmount - userPostAmount;
|
||||
final rawAmount = diff.abs();
|
||||
|
||||
final amountInString = rawAmount.toStringAsFixed(6);
|
||||
amount = double.parse(amountInString);
|
||||
|
||||
isOutgoing = diff > 0;
|
||||
|
||||
if (mintAddress == null && instruction.accounts.length >= 4) {
|
||||
final mintIndex = instruction.accounts[3];
|
||||
mintAddress = message.accountKeys[mintIndex].address;
|
||||
}
|
||||
|
||||
final sender = message.accountKeys[instruction.accounts[0]].address;
|
||||
final receiver = message.accountKeys[instruction.accounts[1]].address;
|
||||
|
||||
String? tokenSymbol = splTokenSymbol;
|
||||
|
||||
if (tokenSymbol == null && mintAddress != null) {
|
||||
final token = await getTokenInfo(mintAddress);
|
||||
tokenSymbol = token?.symbol;
|
||||
}
|
||||
|
||||
return SolanaTransactionModel(
|
||||
isOutgoingTx: isOutgoing,
|
||||
from: sender,
|
||||
to: receiver,
|
||||
id: signature,
|
||||
amount: amount,
|
||||
programId: SPLTokenProgramConst.tokenProgramId.address,
|
||||
blockTimeInInt: blockTime?.toInt() ?? 0,
|
||||
tokenSymbol: tokenSymbol ?? '',
|
||||
fee: feeInSol,
|
||||
);
|
||||
}
|
||||
|
||||
/// Load the Address's transactions into the account
|
||||
Future<List<SolanaTransactionModel>> fetchTransactions(
|
||||
SolAddress address, {
|
||||
|
@ -381,11 +441,13 @@ class SolanaWalletClient {
|
|||
|
||||
transactions.addAll(parsedTransactions.whereType<SolanaTransactionModel>().toList());
|
||||
|
||||
// Calling the callback after each batch is processed, therefore passing the current list of transactions.
|
||||
onUpdate(List<SolanaTransactionModel>.from(transactions));
|
||||
// Only update UI if we have new valid transactions
|
||||
if (parsedTransactions.isNotEmpty) {
|
||||
onUpdate(List<SolanaTransactionModel>.from(transactions));
|
||||
}
|
||||
|
||||
if (i + batchSize < signatures.length) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -732,19 +794,24 @@ class SolanaWalletClient {
|
|||
SolanaAccountInfo? accountInfo;
|
||||
try {
|
||||
accountInfo = await _provider!.request(
|
||||
SolanaRPCGetAccountInfo(account: associatedTokenAccount.address),
|
||||
SolanaRPCGetAccountInfo(
|
||||
account: associatedTokenAccount.address,
|
||||
commitment: Commitment.confirmed,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
accountInfo = null;
|
||||
}
|
||||
|
||||
// If aacountInfo is null, signifies that the associatedTokenAccount has only been created locally and not been broadcasted to the blockchain.
|
||||
// If account exists, we return the associated token account
|
||||
if (accountInfo != null) return associatedTokenAccount;
|
||||
|
||||
if (!shouldCreateATA) return null;
|
||||
|
||||
final payerAddress = payerPrivateKey.publicKey().toAddress();
|
||||
|
||||
final createAssociatedTokenAccount = AssociatedTokenAccountProgram.associatedTokenAccount(
|
||||
payer: payerPrivateKey.publicKey().toAddress(),
|
||||
payer: payerAddress,
|
||||
associatedToken: associatedTokenAccount.address,
|
||||
owner: ownerAddress,
|
||||
mint: mintAddress,
|
||||
|
@ -753,19 +820,23 @@ class SolanaWalletClient {
|
|||
final blockhash = await _getLatestBlockhash(Commitment.confirmed);
|
||||
|
||||
final transaction = SolanaTransaction(
|
||||
payerKey: payerPrivateKey.publicKey().toAddress(),
|
||||
payerKey: payerAddress,
|
||||
instructions: [createAssociatedTokenAccount],
|
||||
recentBlockhash: blockhash,
|
||||
type: TransactionType.v0,
|
||||
);
|
||||
|
||||
transaction.sign([payerPrivateKey]);
|
||||
final serializedTransaction = await _signTransactionInternal(
|
||||
ownerPrivateKey: payerPrivateKey,
|
||||
transaction: transaction,
|
||||
);
|
||||
|
||||
await sendTransaction(
|
||||
serializedTransaction: transaction.serializeString(),
|
||||
serializedTransaction: serializedTransaction,
|
||||
commitment: Commitment.confirmed,
|
||||
);
|
||||
|
||||
// Delay for propagation on the blockchain for newly created associated token addresses
|
||||
// Wait for confirmation
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
return associatedTokenAccount;
|
||||
|
@ -890,7 +961,7 @@ class SolanaWalletClient {
|
|||
}) async {
|
||||
/// Sign the transaction with the owner's private key.
|
||||
final ownerSignature = ownerPrivateKey.sign(transaction.serializeMessage());
|
||||
|
||||
|
||||
transaction.addSignature(ownerPrivateKey.publicKey().toAddress(), ownerSignature);
|
||||
|
||||
/// Serialize the transaction.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue